Focus Zone Behavior

The focusZone behavior and useFocusZone hook are used to designate a container where focus can be moved using keys other than Tab. This is useful for implementing many of the patterns described in Section 6 of the WAI-ARIA Authoring Practices document. The most common use case of this behavior is to allow arrow keys (up and down or left and right) to move focus between related elements, such as items in a menu.

At a high level, the focusZone behavior works by adjusting the tabindex attribute on focusable elements and setting up event listeners on the container that respond to the relevant key presses.

Settings for this behavior allow the user to customize several aspects of the focus zone. See FocusZoneSettings below for a detailed description of these settings.

Focusability: which elements participate in the Focus Zone?

When the focusZone behavior is established, it will discover all focusable elements within the given container and allow them to be focused with the bound keys. Focusable elements are those that either are normally focusable via the Tab key OR have a valid tabindex attribute (including "-1"). The easiest way to ensure an element participates in the focus zone is by applying the attribute tabindex="-1". If you need to prevent a focusable element from participating in the focus zone, you can provide a focusableElementFilter.

Focus order

The focusZone behavior uses the natural DOM ordering of elements to determine focus order. This means that a key that moves focus will either move to a "next" element or to a "previous" element. For example, the left arrow key and the up arrow key would both move focus to the previous element in the DOM order, while the right arrow key and the down arrow key would both move focus to the next element in the DOM order.

Focus cannot be moved beyond the last element of the container (other than using the Tab key). The focusOutBehavior option can be used to allow focus to wrap around from last to first element (or vice-versa).

For a more customized focus movement behavior, the consumer has the ability to supply a custom callback that identifies the next element to focus.

Entering the focus zone

By default, when focus enters a focus zone, the element that receives focus will be the most recently-focused element within that focus zone. If no element had previously been focused, or if that previously-focused element was removed, focus will revert to the first focusable element within the focus zone, regardless of the direction of focus movement.

Using the focusInStrategy option, you can change this behavior. Setting this option to "first" will simply cause the first focusable element in the container to be focused whenever focus enters the focus zone. Setting it to "closest" will cause either the first or last focusable element in the container to be focused depending on the direction of focus movement (for example, a shift+tab that brings focus to the container will cause the last focusable element to be focused, whereas a regular tab would cause the first focusable element to be focused). Otherwise, you may provide a callback to choose a custom element to receive initial focus. One scenario where this would be useful is if you wanted to focus an item that is "selected" in a list.

For more information on choosing the right focus in behavior, see 6.6 Keyboard Navigation Inside Components from the ARIA Authoring Practices document.

Supported keys

The focusZone behavior supports different sets of keys for moving focus around. The bindKeys option is used to set which of the following keys can be used to move focus.

Key(s)NotesUse case
ArrowVerticalPrevents default behavior of scrolling where applicableMost focus zones with vertically-positioned elements
ArrowHorizontalPrevents default behavior of scrolling where applicableMost focus zones with horizontally-positioned elements
HomeAndEndCauses focus to jump to the first or last focusable item in the container. Does not move focus if the currently-focused element is a text box.Generally used with arrow keys
PageUpDownWorks the same as the Home and End keys. Advisable only when supplying a custom callback that supports paging.In a long, scrollable list
Tab/Shift+TabUnlike other keys, the Tab key will always allow movement outside of the focus zone (use the Focus Trap behavior to prevent this). Tab moves to the next item, Shift+Tab moves to the previous.Bind Tab if you want to continue allowing tab to move focus between elements in your container rather than jumping out of the container.
JKJ moves focus to the next item, K moves to the previous. Does not move focus if the currently-focused element is a text box. Originally from the vi keybindingsUsed in certain lists
HLH moves focus to the previous item, L moves to the next. Does not move focus if the currently-focused element is a text box. Originally from the vi keybindingsUsed in certain lists
WSW moves focus to the previous item, S moves to the next. Does not move focus if the currently-focused element is a text box.Any situation where it is more ergonomic for the left hand to perform this action, such as in a gaming context (rare)
ADA moves focus to the previous item, D moves to the next. Does not move focus if the currently-focused element is a text box.Any situation where it is more ergonomic for the left hand to perform this action, such as in a gaming context (rare)

DOM Focus vs. Active Descendant

The focusZone behavior supports two modes of operation: DOM Focus and Active Descendant.

  • DOM Focus is the default mode and by far the most commonly needed. When a key is used to move focus, we call .focus() directly on the element to receive focus. This results in document.activeElement getting set to this new element, and it will receive any necessary styles via :focus and :focus-within.
  • Active Descendant mode does not move DOM focus. Instead, focus remains on the control element, and its aria-activedescendant attribute is set to the ID of the relevant element. Because there are no :focus styles applied and no focus events fired, you can supply an onActiveDescendantChanged callback to handle any necessary styles or other logic as the active descendant changes. For more information on the Active Descendant focus pattern, see 6.6.2 Managing Focus in Composites Using aria-activedescendant from the ARIA Authoring Practices document.

Demo

Deploy Storybook to see live demos of focusZone.

Usage

const settings = {
bindKeys: FocusKeys.ArrowVertical | FocusKeys.HomeAndEnd,
} as FocusZoneSettings
const focusZone = document.getElementById('focusZoneContainer')
focusZone(focusZone, settings)

API

The focusZone function takes the following arguments.

NameTypeDefaultDescription
containerElementThe focus zone will apply to this container and all of its focusable descendants.
settingsFocusZoneSettings{}Settings to customize the focus zone. See below for a description of each setting.

FocusZoneSettings interface

FocusZoneSettings is an object with the following interface. All properties are optional and have default behaviors.

NameTypeDefaultDescription
bindKeysFocusKeys (numeric enum)FocusKeys.ArrowVertical │ FocusKeys.HomeAndEndBit flags that identify keys that will move focus around the focus zone. Each available key either moves focus to the "next", "previous", "start", or "end" element, so it is best to only bind the keys that make sense to move focus in your UI. Use the FocusKeys object to discover supported keys (listed in the "Supported keys" section above).

Use the bitwise "OR" operator (|) to combine key types. For example, FocusKeys.WASD │ FocusKeys.HJKL represents all of W, A, S, D, H, J, K, and L.

The default for this setting is FocusKeys.ArrowVertical │ FocusKeys.HomeAndEnd, unless getNextFocusable is provided, in which case FocusKeys.ArrowAll │ FocusKeys.HomeAndEnd is used as the default.
focusOutBehavior"stop" │ "wrap""stop"Choose the behavior applied in cases where focus is currently at either the first or last element of the container. "stop" - do nothing and keep focus where it was; "wrap" - wrap focus around to the first element from the last, or the last element from the first
focusInStrategy"first" │ "closest" │ "previous" │ Function"previous"This option allows customization of the behavior that determines which of the focusable elements should be focused when focus enters the container via the Tab key.

When set to "first", whenever focus enters the container via Tab, we will focus the first focusable element. When set to "previous", the most recently focused element will be focused (fallback to first if there was no previous).

The "closest" strategy works like "first", except either the first or the last element of the container will be focused, depending on the direction from which focus comes.

If a function is provided, this function should return the HTMLElement intended to receive focus. This is useful if you want to focus the currently "selected" item or element.
getNextFocusableFunctionThis is a callback used to customize the next element to focus when a bound key is pressed. The function takes 3 arguments: direction ("previous", "next", "start", or "end"), from (Element or undefined), and event (KeyboardEvent). The function should return the next element to focus, or undefined. If undefined is returned, the regular algorithm to select the next element to focus will be used.
focusableElementFilterFunctionThis is a callback used to cull focusable elements from participating in the focus zone.