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.
Using the focusInStrategy
option , you can change this behavior.
For more information on choosing the right focus in behavior, see 6.6 Keyboard Navigation Inside Components from the ARIA Authoring Practices document.
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.
Before zone Focus can be moved with up and down arrow keys in here.
First Second Third After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
export default function Default ( ) {
const { containerRef } = useFocusZone ( )
return (
< >
< button > Before zone </ button >
< div
ref = { containerRef as React . 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.
Before zone Focus can be moved with J and K keys.
First Second Third After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
import { FocusKeys } from '@primer/behaviors'
export default function BindKeys ( ) {
const { containerRef } = useFocusZone ( { bindKeys : FocusKeys . JK } )
return (
< >
< button > Before zone </ button >
< div
ref = { containerRef as React . 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 Before zone 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".
First Second Third After zone focusInStrategy: first Before zone Focus can be moved with up and down arrow keys in here.
"First" will always be focused first when focus enters.
First Second Third After zone focusInStrategy: previous Before zone Focus can be moved with up and down arrow keys in here.
The most recently focused element will be focused first when focus enters.
First Second Third After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
export default function FocusInStrategy ( ) {
const { containerRef : containerRefClosest } = useFocusZone ( { focusInStrategy : 'closest' } )
const { containerRef : containerRefFirst } = useFocusZone ( { focusInStrategy : 'first' } )
const { containerRef : containerRefPrevious } = useFocusZone ( { focusInStrategy : 'previous' } )
return (
< >
< div style = { { margin : '1rem 0' } } >
< h4 > focusInStrategy: closest </ h4 >
< button > Before zone </ button >
< div
ref = { containerRefClosest as React . 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 >
< div style = { { margin : '1rem 0' } } >
< h4 > focusInStrategy: first </ h4 >
< button > Before zone </ button >
< div
ref = { containerRefFirst as React . 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 >
< div style = { { margin : '1rem 0' } } >
< h4 > focusInStrategy: previous </ h4 >
< button > Before zone </ button >
< div
ref = { containerRefPrevious as React . 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 Before zone Focus can be moved with up and down arrow keys in here. Focus movement stops at the beginning and end.
First Second Third After zone focusInStrategy: wrap Before zone 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.
First Second Third After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
export default function FocusOutBehavior ( ) {
const { containerRef : containerRefStop } = useFocusZone ( { focusOutBehavior : 'stop' } )
const { containerRef : containerRefWrap } = useFocusZone ( { focusOutBehavior : 'wrap' } )
return (
< >
< div style = { { margin : '1rem 0' } } >
< h4 > focusInStrategy: stop </ h4 >
< button > Before zone </ button >
< div
ref = { containerRefStop as React . 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 >
< div style = { { margin : '1rem 0' } } >
< h4 > focusInStrategy: wrap </ h4 >
< button > Before zone </ button >
< div
ref = { containerRefWrap as React . 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 >
</ >
)
}
Before zone 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.
First Second Third Fourth Fifth Sixth Seventh Eighth Ninth
After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
function getSiblingIndex ( element : Element ) {
let child : Element | null = element
let i = 0
while ( ( child = child . previousElementSibling ) != null ) {
++ i
}
return i
}
export default function GetNextFocusable ( ) {
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 = currentIndex
if ( [ 'End' , 'ArrowRight' ] . includes ( event . key ) ) {
while ( nextIndex % 3 !== 2 ) {
nextIndex += 1
if ( ! toEnd ) {
break
}
}
}
if ( [ 'Home' , 'ArrowLeft' ] . includes ( event . key ) ) {
while ( nextIndex % 3 !== 0 ) {
nextIndex -= 1
if ( ! toEnd ) {
break
}
}
}
if ( event . key === 'ArrowUp' ) {
while ( nextIndex - 3 >= 0 ) {
nextIndex -= 3
if ( ! toEnd ) {
break
}
}
}
if ( event . key === 'ArrowDown' ) {
while ( nextIndex + 3 < 9 ) {
nextIndex += 3
if ( ! toEnd ) {
break
}
}
}
return containerRef . current . children [ nextIndex ] as HTMLElement
}
} ,
[containerRef],
)
useFocusZone( { containerRef , getNextFocusable } )
return (
< >
< button > Before zone </ button >
< div style = { { 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 >
< div
ref = { containerRef as React . 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 >
</ >
)
}
If you need to prevent a focusable element from participating in the focus zone, you can provide a focusableElementFilter
.
Before zone Focus can be moved with up and down arrow keys in here, but links will be skipped unless using the Tab key.
First Link one Second Link two Third After zone code editor import React from 'react'
import { Link , useFocusZone } from '@primer/react'
export default function FocusableElementFilter ( ) {
const { containerRef } = useFocusZone ( { focusableElementFilter : ( element : Element ) => element . tagName === 'BUTTON' } )
return (
< >
< button > Before zone </ button >
< div
ref = { containerRef as React . 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 >
< Link href = " #someLink " > Link one </ Link >
< button > Second </ button >
< Link href = " #someLink " > Link two </ Link >
< button > Third </ button >
</ div >
< button > After zone </ button >
</ >
)
}
Before zone Focus can be moved with up and down arrow keys in here, and the container will not scroll when focus moves.
First Second Third Fourth Fifth Sixth Seventh Eighth Ninth After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
export default function PreventScroll ( ) {
const { containerRef } = useFocusZone ( { preventScroll : true } )
return (
< >
< button > Before zone </ button >
< div
ref = { containerRef as React . 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 >
</ >
)
}
Disable focus zone Before zone
When the focus zone is enabled, focus can be moved with up and down arrow keys in here.
First Second Third After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
export default function Disable ( ) {
const [ fzEnabled , setFzEnabled ] = React . useState ( true )
const { containerRef } = useFocusZone ( { disabled : ! fzEnabled } )
const toggleFz = React . useCallback ( ( ) => {
setFzEnabled ( ! fzEnabled )
} , [ fzEnabled ] )
return (
< >
< div
style = { {
display : 'flex' ,
gap : '1rem' ,
} }
>
< button onClick = { toggleFz } > { fzEnabled ? 'Disable' : 'Enable' } focus zone </ button >
< button > Before zone </ button >
</ div >
< div
ref = { containerRef as React . 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.
code editor import React from 'react'
import { FormControl , TextInput , useFocusZone } from '@primer/react'
import { FocusKeys } from '@primer/behaviors'
export default function Default ( ) {
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 instanceof HTMLButtonElement ,
} )
return (
< >
< FormControl >
< FormControl.Label > Focus and move the active descendant w/ up and down arrow keys </ FormControl.Label >
< TextInput ref = { controllingElementRef } value = " " style = { { width : 500 } } />
</ FormControl >
< div
ref = { 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 >
</ >
)
}
Before zone Focus can be moved with arrow up and arrow down keys in here.
First Second Third Focus can be moved with up, down, left, or right arrow keys in here.
First nested Second nested Third nested
After zone code editor import React from 'react'
import { useFocusZone } from '@primer/react'
import { FocusKeys } from '@primer/behaviors'
export default function NestingFocusZones ( ) {
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 >
< div
ref = { 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 >
< div style = { { border : '1px solid gray' , margin : '1rem' , padding : '1rem' } } >
< p > Focus can be moved with up, down, left, or right arrow keys in here. </ p >
< div ref = { 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 >
</ >
)
}
Loading data for useFocusZone...