useFocusZone
Manage focus across multiple zones with arrow key and Tab transitions. Supports controlled and uncontrolled zone state.
useFocusZone models a layout as a set of named zones and moves focus between them on arrow keys or Tab. It tracks which zone is active, optionally drives DOM focus to a target per zone, and hands back getKeyOptions so each zone's own shortcuts only fire while that zone holds focus. It is generic over the zone type — use as const arrays for full inference of zone names.
When to use it
- A composite layout (sidebar + main, toolbar + canvas, search + results + preview) where arrow keys or Tab should jump between regions.
- A region whose internal keyboard handlers should be gated on that region being active.
- Two-zone row patterns — though for "content plus inline actions" rows, reach for the purpose-built
useActionRowNavigation, which is built on this hook.
For movement within a single list, use useNavigation or useScopedNavigation. useFocusZone handles movement between regions. See the focus zones guide.
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
Returns
Keyboard behavior
When the focus option is set, changing zones moves DOM focus to the zone's target (a ref, a getter, or a { container, target } pair). Initial mount does not steal focus unless autoFocus is true. getZoneProps(zone) returns data-focused for styling the active region.
The playground example registers its layout container and a focus target for every panel. Tab therefore changes both the active zone and DOM focus only while focus is inside the example; Tab elsewhere on the page remains native.
Examples
Multi-zone layout navigation
Tab-cycle regions with focus targets
Notes
Generic over zone type
useFocusZone is generic over T extends string. Use `as const` arrays for full type inference of zone names.
Controlled and uncontrolled
Pass zone + onZoneChange for controlled mode, or rely on initial for uncontrolled mode.
Scoped DOM handling
Use containerRef + focusWithinOnly when multiple keyboard regions share a page-level scope.
Requires KeyboardProvider
useFocusZone registers key bindings through the provider context and must be used within a <KeyboardProvider> tree.
Edge cases
- Controlled mode. Pass
zone+onZoneChangeto own the active zone; omitzoneand rely oninitialfor uncontrolled state. - Shared page scope. When several zones share one provider scope, pass
containerRef+focusWithinOnlyso transitions only fire while focus is inside the layout. focuswith{ container, target }. The pair form skips focus repair while focus is already insidecontainer, which avoids yanking focus away from a control the user is interacting with.
Hazard: tabCycle can swallow Tab document-wide. tabCycle registers window-level Tab/Shift+Tab handlers. The cycle is only scoped to the layout when the hook can resolve a containing element — either every cycled zone supplies a focus.targets container, or you pass an explicit containerRef. With containment resolvable, Tab only cycles (and only calls preventDefault) while focus is inside one of those containers; outside them it declines so native Tab keeps reaching the skip link, header controls, toasts, and other tabbable elements. If you configure tabCycle with no focus.targets containers and no containerRef, Tab is intercepted across the whole document for as long as the zone is mounted — a WCAG 2.1.1 hazard. Always anchor every cycled zone to a container.
For screen-level layouts where Tab is deliberately the "switch pane" key everywhere, opt in with tabCycleScope: "document". It makes the intercept explicit rather than accidental: Tab cycles zones from anywhere in the document while the zone's scope is active, moves focus to the active zone's target, and still leaves native Tab intact on editable targets so typing in inputs is never trapped.
Add tabCycleBoundary when document-scope cycling should only apply inside one region, such as a page's <main>. While focus is outside the resolved boundary, native Tab proceeds. Omitting the option or returning null keeps the document-wide behavior.
Related
useActionRowNavigation— a two-zone row built onuseFocusZone.useScopedNavigation— move within a zone once focus lands there.- Focus zones guide — transitions, tab cycling, and focus targets.
Source
Highlighted source loads after this disclosure opens. Browse the source repository.