Skip to content

useActionRowNavigation

provider-dependentnavigationactionsrow

Provider-backed two-zone keyboard navigation for rows with inline actions. Moves between row content and action buttons while preserving disabled-action behavior.

useActionRowNavigation coordinates a row's main content with a strip of inline action buttons. It splits the row into two zones — content and actions — and wires the arrow keys so the keyboard can step into the actions, move between them, activate one, and step back out. It is built for dense list, table, inbox, review, and command-result rows.

It composes useFocusZone (the two zones) with useKey (the per-zone bindings), so it requires KeyboardProvider and is package-only.

tsx
const containerRef = useRef<HTMLDivElement>(null)const row = useActionRowNavigation({  enabled: true,  actionCount: 3,  containerRef,  onAction: (index) => runAction(index),})return <div ref={containerRef}>{/* row content and actions */}</div>

When to use it

  • A row has both a primary target (open, navigate) and secondary actions (approve, archive, delete) that should be reachable without a mouse.
  • Actions live to the side of the row content and should be entered with a single arrow press, not by Tabbing through every button on the page.
  • Some actions are conditionally disabled and keyboard movement must skip them rather than landing on dead controls.

For a plain list with no per-row action strip, use useNavigation or useScopedNavigation instead.

Installation

Requires KeyboardProvider and the @diffgazer/keys package, which is not public on npm yet.

Diffgazer packages are not yet published to npm. Until the first release, pack @diffgazer/ui and @diffgazer/keys from the repository and install those tarballs.

@diffgazer/keys requires no CSS or Tailwind setup.

Parameters

NameTypeDefaultDescription
enabledrequiredbooleanEnables row keyboard handling. Pass the row's active/selected state.
actionCountrequirednumberNumber of action controls managed by the row.
onActionrequired(index: number) => voidCalled when Enter or Space activates the focused action.
disabledActionsreadonly boolean[]Per-index disabled flags. Disabled actions are skipped during navigation and ignored on activation.
actionIdsreadonly string[]Stable per-index control identities. When provided, a rebuild that replaces the control at the focused index (same index, still enabled) counts as displacement, so the hook repairs the dropped DOM focus onto the new element instead of leaving it on the body. The hook keys on the id values, not the array identity, so an inline `.map` per render is fine.
disabledFocusFallbackRefRefObject<HTMLElement | null>Content focus target used both when entering the actions zone with no action enabled and when ArrowUp exits the actions zone. Omitting it means exiting only blurs the focused action, so focus lands on document.body.
containerRefRefObject<HTMLElement | null>When supplied, limits zone handling to one row subtree so sibling rows do not respond to the same key press. Omit it only when the active row should keep global keyboard ownership within its scope.
scopestring | nullKeyboard scope name to register the row shortcuts under. Omit to use implicit scope ordering; pass null to skip registration.
allowInInputbooleanfalseAllow row shortcuts to run when an editable element is focused.
wrapbooleanfalseWrap ArrowLeft/ArrowRight movement across the action ends.
defaultZone"content" | "actions""content"Initial zone. When set to actions, the hook focuses the default action on mount.
defaultIndexnumber0Initial action index focused when entering the actions zone.
canExitActionsbooleantrueAllow ArrowUp to leave the actions zone back to content.
onNavigate(index: number) => voidCalled when navigation lands on an action index, including when entering the actions zone.
onNavigationBoundaryReached(direction: "previous" | "next") => voidCalled when navigation cannot move further: previous when exiting actions or hitting the left edge with wrap off, next when hitting the right edge.

Returns

UseActionRowNavigationReturnZone state plus imperative controls and a getActionProps factory to wire each action element.
NameTypeDefaultDescription
inActionsrequiredbooleanWhether the row is currently in the actions zone.
focusedIndexrequirednumberCurrent focused action index.
isFocusedActionDisabledrequiredbooleanWhether the current action index is disabled.
enterActionsrequired(index?: number) => number | nullMoves into the actions zone and focuses the requested or first enabled action. Returns the focused index, or null when no action is enabled.
exitActionsrequired() => voidReturns to the content zone when canExitActions is true, resets the focused action index to 0, and moves focus to disabledFocusFallbackRef (or blurs the action when that ref is omitted).
resetrequired(initialIndex?: number) => voidReturns to content and resets the focused action index.
isRegisteredActionFocusedrequired() => booleanWhether DOM focus currently rests on the registered action at focusedIndex. The zone can stay actions after focus left it (Shift+Tab, a click away), so consumers gating their own shortcuts on the row should check this rather than the zone.
getActionPropsrequired(index: number) => { ref: RefCallback<HTMLElement>; "data-action-index": number; onFocus: () => void }Props to spread onto each action element so the hook can focus and track it.

Keyboard behavior

KeyZoneBehavior
ArrowDowncontentEnters the actions zone and focuses the first enabled action.
ArrowLeftactionsMoves to the previous enabled action. At the left edge it wraps (when wrap) or reports the "previous" boundary.
ArrowRightactionsMoves to the next enabled action. At the right edge it wraps (when wrap) or reports the "next" boundary.
ArrowUpactionsExits the actions zone back to content when canExitActions is true, moving focus to disabledFocusFallbackRef. Without that ref it only blurs the action, so focus lands on document.body.
Enter / SpaceactionsCalls onAction(focusedIndex) for the focused, enabled action.

When containerRef is supplied, the row only handles these keys while focus is inside that container. Omitting containerRef intentionally leaves the active row's handlers global within the current keyboard scope. Bindings register through KeyboardProvider, so they participate in the scope stack.

Examples

Review row with inline actions

Preview

Notes

Two zones, one row

The hook composes useFocusZone with a content zone and an actions zone. ArrowDown enters actions from content, ArrowUp leaves actions, and ArrowLeft/ArrowRight move between actions.

Disabled actions

Indexes flagged in disabledActions are skipped during ArrowLeft/ArrowRight movement and ignored by Enter/Space. If every action is disabled, the hook returns to content and focuses disabledFocusFallbackRef.

Scope rows with containerRef

Pass containerRef for each row so its arrow keys only fire while focus is inside that row. Omitting it intentionally leaves the active row's handlers global within the current keyboard scope.

Requires KeyboardProvider

useActionRowNavigation registers keys through useKey and useFocusZone, so it must be used within a <KeyboardProvider> tree. It is package-only.

Edge cases

  • All actions disabled. Entering the actions zone with no enabled action returns focus to disabledFocusFallbackRef, or blurs the focused action when the ref is omitted. enterActions returns null in this case.
  • Disabled flags change at runtime. Update disabledActions and the hook re-resolves the focused index on the next render; if the focused action becomes disabled it moves to the first enabled one, or leaves the zone when none remain.
  • getActionProps is required per action. The hook focuses and tracks actions through the ref and onFocus it returns. Spread it onto every action element, including disabled ones, so indexes stay aligned.
  • useFocusZone — the underlying two-zone primitive.
  • useNavigation — single-zone list navigation without an action strip.
  • useKey — the per-zone key registration this hook builds on.

Source

Highlighted source loads after this disclosure opens. Browse the source repository.