Designates a container where focus can be moved using keys other than 'Tab'.
This hook 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, useFocusZone works by adjusting the tabindex attribute on focusable elements and setting up event listeners on the container that respond to the relevant key presses.
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".
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.
The useFocusZone hook 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.
Focus can be moved with up and down arrow keys in here.
importReactfrom'react'import{useFocusZone}from'@primer/react'exportdefaultfunctionDefault(){const{containerRef}=useFocusZone()return(<><button>Before zone</button><divref={containerRef asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>Focus can be moved with up and down arrow keys in here.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></>)}
The bindKeys option is used to set which of the following keys can be used to move focus.
This example shows how to use the bindKeys option to allow focus to be moved using the J and K keys.
Focus can be moved with J and K keys.
importReactfrom'react'import{useFocusZone}from'@primer/react'import{FocusKeys}from'@primer/behaviors'exportdefaultfunctionBindKeys(){const{containerRef}=useFocusZone({bindKeys:FocusKeys.JK})return(<><button>Before zone</button><divref={containerRef asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',gap:'1rem'}}><p>Focus can be moved with J and K keys.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></>)}
Setting focusInStrategy 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.
focusInStrategy: closest
Focus can be moved with up and down arrow keys in here.
"First" will be focused first when focus enters from "Before zone". "Third" will be focused first when focus enters from "After zone".
focusInStrategy: first
Focus can be moved with up and down arrow keys in here.
"First" will always be focused first when focus enters.
focusInStrategy: previous
Focus can be moved with up and down arrow keys in here.
The most recently focused element will be focused first when focus enters.
importReactfrom'react'import{useFocusZone}from'@primer/react'exportdefaultfunctionFocusInStrategy(){const{containerRef: containerRefClosest}=useFocusZone({focusInStrategy:'closest'})const{containerRef: containerRefFirst}=useFocusZone({focusInStrategy:'first'})const{containerRef: containerRefPrevious}=useFocusZone({focusInStrategy:'previous'})return(<><divstyle={{margin:'1rem 0'}}><h4>focusInStrategy: closest</h4><button>Before zone</button><divref={containerRefClosest asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>Focus can be moved with up and down arrow keys in here.</p><p> "First" will be focused first when focus enters from "Before zone". "Third" will be focused first when focus enters from "After zone".</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></div><divstyle={{margin:'1rem 0'}}><h4>focusInStrategy: first</h4><button>Before zone</button><divref={containerRefFirst asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>Focus can be moved with up and down arrow keys in here.</p><p>"First" will always be focused first when focus enters.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></div><divstyle={{margin:'1rem 0'}}><h4>focusInStrategy: previous</h4><button>Before zone</button><divref={containerRefPrevious asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>Focus can be moved with up and down arrow keys in here.</p><p>The most recently focused element will be focused first when focus enters.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></div></>)}
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.
focusInStrategy: stop
Focus can be moved with up and down arrow keys in here. Focus movement stops at the beginning and end.
focusInStrategy: wrap
Focus can be moved with up and down arrow keys in here. Focus movement will wrap back around to the first or last element after reaching the beginning or end.
importReactfrom'react'import{useFocusZone}from'@primer/react'exportdefaultfunctionFocusOutBehavior(){const{containerRef: containerRefStop}=useFocusZone({focusOutBehavior:'stop'})const{containerRef: containerRefWrap}=useFocusZone({focusOutBehavior:'wrap'})return(<><divstyle={{margin:'1rem 0'}}><h4>focusInStrategy: stop</h4><button>Before zone</button><divref={containerRefStop asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p> Focus can be moved with up and down arrow keys in here.<br/> Focus movement stops at the beginning and end.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></div><divstyle={{margin:'1rem 0'}}><h4>focusInStrategy: wrap</h4><button>Before zone</button><divref={containerRefWrap asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p> Focus can be moved with up and down arrow keys in here.<br/> Focus movement will wrap back around to the first or last element after reaching the beginning or end.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></div></>)}
Focus can be moved in rows with left and right arrow keys. Focus can be moved in columns with the up and down arrow keys.
importReactfrom'react'import{useFocusZone}from'@primer/react'functiongetSiblingIndex(element:Element){letchild:Element|null= elementlet i =0while((child = child.previousElementSibling)!=null){++i}return i}exportdefaultfunctionGetNextFocusable(){const containerRef =React.useRef<HTMLElement>(null) const getNextFocusable = React.useCallback( ( direction: 'previous' | 'next' | 'start' | 'end', from: Element | undefined, event: KeyboardEvent, ): HTMLElement | undefined => {const toEnd = direction ==='start'|| direction ==='end'if(from&& containerRef.current){const currentIndex =getSiblingIndex(from)let nextIndex = currentIndexif(['End','ArrowRight'].includes(event.key)){while(nextIndex %3!==2){ nextIndex +=1if(!toEnd){break}}}if(['Home','ArrowLeft'].includes(event.key)){while(nextIndex %3!==0){ nextIndex -=1if(!toEnd){break}}}if(event.key==='ArrowUp'){while(nextIndex -3>=0){ nextIndex -=3if(!toEnd){break}}}if(event.key==='ArrowDown'){while(nextIndex +3<9){ nextIndex +=3if(!toEnd){break}}}return containerRef.current.children[nextIndex]asHTMLElement}}, [containerRef], ) useFocusZone({containerRef, getNextFocusable}) return (<><button>Before zone</button><divstyle={{border:'1px solid',margin:'1rem',padding:'1rem'}}><p> Focus can be moved in rows with left and right arrow keys. Focus can be moved in columns with the up and down arrow keys.</p><divref={containerRef asReact.RefObject<HTMLDivElement>}style={{display:'grid',gridTemplateRows:'repeat(3, 1fr)',gridTemplateColumns:'repeat(3, 1fr)',gap:'0.5rem',}}><button>First</button><button>Second</button><button>Third</button><button>Fourth</button><button>Fifth</button><button>Sixth</button><button>Seventh</button><button>Eighth</button><button>Ninth</button></div></div><button>After zone</button></> )}
importReactfrom'react'import{useFocusZone}from'@primer/react'exportdefaultfunctionFocusableElementFilter(){const{containerRef}=useFocusZone({focusableElementFilter:(element:Element)=> element.tagName==='BUTTON'})return(<><button>Before zone</button><divref={containerRef asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p> Focus can be moved with up and down arrow keys in here, but <em>links will be skipped</em> unless using the Tab key.</p><button>First</button><ahref="#someLink">Link one</a><button>Second</button><ahref="#someLink">Link two</a><button>Third</button></div><button>After zone</button></>)}
Focus can be moved with up and down arrow keys in here, and the container will not scroll when focus moves.
importReactfrom'react'import{useFocusZone}from'@primer/react'exportdefaultfunctionPreventScroll(){const{containerRef}=useFocusZone({preventScroll:true})return(<><button>Before zone</button><divref={containerRef asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',height:'150px',overflow:'auto',display:'flex',flexDirection:'column',gap:'0.5rem',}}><p> Focus can be moved with up and down arrow keys in here, and the container will not scroll when focus moves.</p><button>First</button><button>Second</button><button>Third</button><button>Fourth</button><button>Fifth</button><button>Sixth</button><button>Seventh</button><button>Eighth</button><button>Ninth</button></div><button>After zone</button></>)}
When the focus zone is enabled, focus can be moved with up and down arrow keys in here.
importReactfrom'react'import{useFocusZone}from'@primer/react'exportdefaultfunctionDisable(){const[fzEnabled, setFzEnabled]=React.useState(true)const{containerRef}=useFocusZone({disabled:!fzEnabled})const toggleFz =React.useCallback(()=>{setFzEnabled(!fzEnabled)},[fzEnabled])return(<><divstyle={{display:'flex',gap:'1rem',}}><buttononClick={toggleFz}>{fzEnabled ?'Disable':'Enable'} focus zone</button><button>Before zone</button></div><divref={containerRef asReact.RefObject<HTMLDivElement>}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>When the focus zone is enabled, focus can be moved with up and down arrow keys in here.</p><button>First</button><button>Second</button><button>Third</button></div><button>After zone</button></>)}
This is the kind of behavior that comboboxes use, where the focus is on the control element, but the active descendant is set to the ID of the relevant element.
The active descendant can be moved in here while the controlling element is focused.
importReactfrom'react'import{useFocusZone}from'@primer/react'import{FocusKeys}from'@primer/behaviors'exportdefaultfunctionDefault(){const containerRef =React.useRef<HTMLDivElement>(null) const controllingElementRef = React.useRef<HTMLInputElement>(null) useFocusZone({ containerRef,activeDescendantFocus: controllingElementRef,bindKeys:FocusKeys.ArrowVertical,onActiveDescendantChanged:(current, previous)=>{if(current){ current.style.outline=`2px solid blue`}if(previous){ previous.style.outline=''}},focusableElementFilter:elem=> elem instanceofHTMLButtonElement,}) return (<><inputref={controllingElementRef}value="Focus and move the active descendant w/ up and down arrow keys"style={{width:500}}/><divref={containerRef}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>The active descendant can be moved in here while the controlling element is focused.</p><button>First</button><button>Second</button><button>Third</button></div></> )}
Focus can be moved with arrow up and arrow down keys in here.
Focus can be moved with up, down, left, or right arrow keys in here.
importReactfrom'react'import{useFocusZone}from'@primer/react'import{FocusKeys}from'@primer/behaviors'exportdefaultfunctionNestingFocusZones(){const outerContainerRef =React.useRef<HTMLDivElement>(null) const innerContainerRef = React.useRef<HTMLDivElement>(null) useFocusZone({containerRef: outerContainerRef,bindKeys:FocusKeys.ArrowVertical,}) useFocusZone({containerRef: innerContainerRef,bindKeys:FocusKeys.ArrowHorizontal,}) return (<><button>Before zone</button><divref={outerContainerRef}style={{border:'1px solid',margin:'1rem',padding:'1rem',display:'flex',flexDirection:'column',gap:'1rem',}}><p>Focus can be moved with arrow up and arrow down keys in here.</p><button>First</button><button>Second</button><button>Third</button><divstyle={{border:'1px solid gray',margin:'1rem',padding:'1rem'}}><p>Focus can be moved with up, down, left, or right arrow keys in here.</p><divref={innerContainerRef}style={{display:'flex',gap:'1rem'}}><button>First nested</button><button>Second nested</button><button>Third nested</button></div></div></div><button>After zone</button></> )}