Focus Zone Behavior
Used to designate a container where focus can be moved using keys other than `Tab`.
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) | Notes | Use case |
---|---|---|
ArrowVertical | Prevents default behavior of scrolling where applicable | Most focus zones with vertically-positioned elements |
ArrowHorizontal | Prevents default behavior of scrolling where applicable | Most focus zones with horizontally-positioned elements |
HomeAndEnd | Causes 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 |
PageUpDown | Works 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+Tab | Unlike 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. |
JK | J 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 keybindings | Used in certain lists |
HL | H 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 keybindings | Used in certain lists |
WS | W 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) |
AD | A 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 indocument.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 nofocus
events fired, you can supply anonActiveDescendantChanged
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 Usingaria-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 FocusZoneSettingsconst focusZone = document.getElementById('focusZoneContainer')focusZone(focusZone, settings)
API
The focusZone
function takes the following arguments.
Name | Type | Default | Description |
---|---|---|---|
container | Element | The focus zone will apply to this container and all of its focusable descendants. | |
settings | FocusZoneSettings | {} | 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.
Name | Type | Default | Description |
---|---|---|---|
bindKeys | FocusKeys (numeric enum) | FocusKeys.ArrowVertical │ FocusKeys.HomeAndEnd | Bit 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. |
getNextFocusable | Function | This 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. | |
focusableElementFilter | Function | This is a callback used to cull focusable elements from participating in the focus zone. |