Composite
Composite
is a component that may contain navigable items represented by CompositeItem
. It's inspired by the WAI-ARIA Composite Role and implements all the keyboard navigation mechanisms to ensure that there's only one tab stop for the whole Composite
element. This means that it can behave as a roving tabindex or aria-activedescendant container.
Since this a very abstract component, it's recommended that you use more concrete ones like Menu
, Toolbar
, TabList
and other derivative components, or build your own on top of Composite
.
#Installation
npm install reakit
Learn more in Get started.
#Usage
In the most basic usage, Composite
will work as a roving tabindex container that works on both directions. That is, it listens to all arrow keys. You can specify a direction by passing an orientation
initial state to useCompositeState
.
import React from "react"; import { useCompositeState, Composite, CompositeItem } from "reakit/Composite"; function Example() { const composite = useCompositeState(); return ( <Composite {...composite} role="toolbar" aria-label="My toolbar"> <CompositeItem {...composite} onClick={() => alert("clicked")}> Item 1 </CompositeItem> <CompositeItem {...composite}>Item 2</CompositeItem> <CompositeItem {...composite}>Item 3</CompositeItem> </Composite> ); }
#aria-activedescendant
Composite
may work as an aria-activedescendant container by just setting the virtual
initial state to true
.
You can still attach event handlers to CompositeItem
just like it were using the roving tabindex method. You don't need to change anything else to make it work.
The
unstable_virtual
prop is still experimental and may change in future patch and minor releases.
import React from "react"; import { useCompositeState, Composite, CompositeItem } from "reakit/Composite"; function Example() { const composite = useCompositeState({ unstable_virtual: true }); return ( <Composite {...composite} role="toolbar" aria-label="My toolbar"> <CompositeItem {...composite} onClick={() => alert("clicked")}> Item 1 </CompositeItem> <CompositeItem {...composite}>Item 2</CompositeItem> <CompositeItem {...composite}>Item 3</CompositeItem> </Composite> ); }
#Two-dimensional navigation
You can build a two-dimensional Composite
by using CompositeGroup
.
import React from "react"; import { useCompositeState, Composite, CompositeGroup, CompositeItem, } from "reakit/Composite"; function Grid(props) { return <Composite role="grid" {...props} />; } function GridRow(props) { return <CompositeGroup role="row" {...props} />; } function GridCell(props) { return <CompositeItem as="div" role="gridcell" {...props} />; } function Example() { const composite = useCompositeState({ wrap: true }); return ( <Grid {...composite} aria-label="My grid"> <GridRow {...composite}> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> </GridRow> <GridRow {...composite}> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> </GridRow> <GridRow {...composite}> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> <GridCell {...composite}>Item</GridCell> </GridRow> </Grid> ); }
#Performance
If you notice performance issues when rendering several CompositeItem
s, you can do the following:
- Pass an
id
prop to eachCompositeItem
. - Memoize all non-primitive props that you're passing to
CompositeItem
, including event handlers (e.g.onClick
) and thechildren
prop.
CompositeItem
will compare the passed id
with composite.currentId
and, if the other props haven't been changed, it'll only re-render if it's the previous or the current active item.
In the example below, focus on any item and keep → pressed to see it smoothly changing focus. Then, unmemoize any non-primitive prop (like onClick
or children
) and do the same to see the difference yourself.
import React from "react"; import { useCompositeState, Composite, CompositeItem } from "reakit/Composite"; const items = Array.from({ length: 88 }).map((_, i) => `item-${i}`); function Example() { const composite = useCompositeState({ loop: true }); // Remove the React.useCallback call below to see the difference const onClick = React.useCallback((event) => { window.alert(event.currentTarget.id); }, []); // If children aren't primitive values (like strings), you can memoize them // with React.useCallback const children = React.useCallback( (itemProps) => <span {...itemProps}>👉</span>, [] ); return ( <Composite {...composite} role="toolbar" aria-label="Performance"> {items.map((id) => ( <CompositeItem {...composite} key={id} id={id} onClick={onClick}> {children} </CompositeItem> ))} </Composite> ); }
#Accessibility
-
When
virtual
is set tofalse
(default):CompositemItem
hastabindex
set to0
if it's the current element. Otherwisetabindex
is set to-1
.
-
When
virtual
is set totrue
:Composite
hastabindex
set to0
and hasaria-activedescendant
set to the id of the currentCompositeItem
.CompositeItem
hasaria-selected
set totrue
if it's the current element.
-
On one-dimensional composites:
- ↑ moves focus to the previous
CompositeItem
iforientation
isvertical
or not defined. - ↓ moves focus to the next
CompositeItem
iforientation
isvertical
or not defined. - → moves focus to the next
CompositeItem
iforientation
ishorizontal
or not defined. - ← moves focus to the previous
CompositeItem
iforientation
ishorizontal
or not defined. - Home or PageUp moves focus to the first
CompositeItem
. - End or PageDown moves focus to the last
CompositeItem
.
- ↑ moves focus to the previous
-
On two-dimensional composites:
- ↑ moves focus to the
CompositeItem
above. - ↓ moves focus to the
CompositeItem
below. - → moves focus to the next
CompositeItem
. - ← moves focus to the previous
CompositeItem
. - Home moves focus to the first
CompositeItem
in the row. - End moves focus to the last
CompositeItem
in the row. - PageUp moves focus to the first
CompositeItem
in the column. - PageDown moves focus to the last
CompositeItem
in the column. - Ctrl+Home moves focus to the first
CompositeItem
in the composite element. - Ctrl+End moves focus to the last
CompositeItem
in the composite element.
- ↑ moves focus to the
Learn more in Accessibility.
#Composition
Composite
uses Tabbable, and is used by TabList, RadioGroup, Menu and Toolbar.CompositeGroup
uses Group and Id.CompositeItem
uses Id and Clickable, and is used by Tab, Radio, MenuItem and ToolbarItem.
Learn more in Composition.
#Props
#useCompositeState
-
baseId
string
ID that will serve as a base for all the items IDs.
-
unstable_virtual
boolean
If enabled, the composite element will act as an aria-activedescendant container instead of roving tabindex. DOM focus will remain on the composite while its items receive virtual focus.
-
rtl
boolean
Determines how
next
andprevious
functions will behave. Ifrtl
is set totrue
, they will be inverted. This only affects the composite widget behavior. You still need to setdir="rtl"
on HTML/CSS. -
orientation
"horizontal" | "vertical" | undefined
Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the
orientation
value determines which arrow keys can be used to move focus:undefined
: all arrow keys work.horizontal
: only left and right arrow keys work.vertical
: only up and down arrow keys work.
It doesn't have any effect on two-dimensional composites.
-
currentId
string | null | undefined
The current focused item
id
.undefined
will automatically focus the first enabled composite item.null
will focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentId
is initially set tonull
, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
loop
boolean | "horizontal" | "vertical"
On one-dimensional composites:
true
loops from the last item to the first item and vice-versa.horizontal
loops only iforientation
ishorizontal
or not set.vertical
loops only iforientation
isvertical
or not set.- If
currentId
is initially set tonull
, the composite element will be focused in between the last and first items.
On two-dimensional composites:
true
loops from the last row/column item to the first item in the same row/column and vice-versa. If it's the last item in the last row, it moves to the first item in the first row and vice-versa.horizontal
loops only from the last row item to the first item in the same row.vertical
loops only from the last column item to the first item in the column row.- If
currentId
is initially set tonull
, vertical loop will have no effect as moving down from the last row or up from the first row will focus the composite element. - If
wrap
matches the value ofloop
, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
-
wrap
boolean | "horizontal" | "vertical"
Has effect only on two-dimensional composites. If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.
true
wraps between rows and columns.horizontal
wraps only between rows.vertical
wraps only between columns.- If
loop
matches the value ofwrap
, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
-
shift
boolean
Has effect only on two-dimensional composites. If enabled, moving up or down when there's no next item or the next item is disabled will shift to the item right before it.
#Composite
-
disabled
boolean | undefined
Same as the HTML attribute.
-
focusable
boolean | undefined
When an element is
disabled
, it may still befocusable
. It works similarly toreadOnly
on form elements. In this case, onlyaria-disabled
will be set.
12 state props
These props are returned by the state hook. You can spread them into this component (
{...state}
) or pass them separately. You can also provide these props from your own state logic.
-
baseId
string
ID that will serve as a base for all the items IDs.
-
unstable_virtual
boolean
If enabled, the composite element will act as an aria-activedescendant container instead of roving tabindex. DOM focus will remain on the composite while its items receive virtual focus.
-
orientation
"horizontal" | "vertical" | undefined
Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the
orientation
value determines which arrow keys can be used to move focus:undefined
: all arrow keys work.horizontal
: only left and right arrow keys work.vertical
: only up and down arrow keys work.
It doesn't have any effect on two-dimensional composites.
-
currentId
string | null | undefined
The current focused item
id
.undefined
will automatically focus the first enabled composite item.null
will focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentId
is initially set tonull
, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
wrap
boolean | "horizontal" | "vertical"
Has effect only on two-dimensional composites. If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.
true
wraps between rows and columns.horizontal
wraps only between rows.vertical
wraps only between columns.- If
loop
matches the value ofwrap
, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
-
unstable_moves
number
Stores the number of moves that have been performed by calling
move
,next
,previous
,up
,down
,first
orlast
. -
groups
Group[]
Lists all the composite groups with their
id
and DOMref
. This state is automatically updated whenregisterGroup
andunregisterGroup
are called. -
items
Item[]
Lists all the composite items with their
id
, DOMref
,disabled
state andgroupId
if any. This state is automatically updated whenregisterItem
andunregisterItem
are called. -
setCurrentId
(value: SetStateAction<string | null | undefine...
Sets
currentId
. This is different fromcomposite.move
as this only updates thecurrentId
state without moving focus. When the composite widget gets focused by the user, the item referred by thecurrentId
state will get focus. -
first
() => void
Moves focus to the first item.
-
last
() => void
Moves focus to the last item.
-
move
(id: string | null) => void
Moves focus to a given item ID.
#CompositeGroup
-
id
string | undefined
Same as the HTML attribute.
6 state props
These props are returned by the state hook. You can spread them into this component (
{...state}
) or pass them separately. You can also provide these props from your own state logic.
-
baseId
string
ID that will serve as a base for all the items IDs.
-
registerGroup
(group: Group) => void
Registers a composite group.
-
unregisterGroup
(id: string) => void
Unregisters a composite group.
-
currentId
string | null | undefined
The current focused item
id
.undefined
will automatically focus the first enabled composite item.null
will focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentId
is initially set tonull
, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
unstable_moves
number
Stores the number of moves that have been performed by calling
move
,next
,previous
,up
,down
,first
orlast
. -
items
Item[]
Lists all the composite items with their
id
, DOMref
,disabled
state andgroupId
if any. This state is automatically updated whenregisterItem
andunregisterItem
are called.
#CompositeItem
-
disabled
boolean | undefined
Same as the HTML attribute.
-
focusable
boolean | undefined
When an element is
disabled
, it may still befocusable
. It works similarly toreadOnly
on form elements. In this case, onlyaria-disabled
will be set. -
id
string | undefined
Same as the HTML attribute.
15 state props
These props are returned by the state hook. You can spread them into this component (
{...state}
) or pass them separately. You can also provide these props from your own state logic.
-
baseId
string
ID that will serve as a base for all the items IDs.
-
unstable_virtual
boolean
If enabled, the composite element will act as an aria-activedescendant container instead of roving tabindex. DOM focus will remain on the composite while its items receive virtual focus.
-
orientation
"horizontal" | "vertical" | undefined
Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the
orientation
value determines which arrow keys can be used to move focus:undefined
: all arrow keys work.horizontal
: only left and right arrow keys work.vertical
: only up and down arrow keys work.
It doesn't have any effect on two-dimensional composites.
-
unstable_moves
number
Stores the number of moves that have been performed by calling
move
,next
,previous
,up
,down
,first
orlast
. -
currentId
string | null | undefined
The current focused item
id
.undefined
will automatically focus the first enabled composite item.null
will focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentId
is initially set tonull
, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
items
Item[]
Lists all the composite items with their
id
, DOMref
,disabled
state andgroupId
if any. This state is automatically updated whenregisterItem
andunregisterItem
are called. -
setCurrentId
(value: SetStateAction<string | null | undefine...
Sets
currentId
. This is different fromcomposite.move
as this only updates thecurrentId
state without moving focus. When the composite widget gets focused by the user, the item referred by thecurrentId
state will get focus. -
first
() => void
Moves focus to the first item.
-
last
() => void
Moves focus to the last item.
-
registerItem
(item: Item) => void
Registers a composite item.
-
unregisterItem
(id: string) => void
Unregisters a composite item.
-
next
(unstable_allTheWay?: boolean | undefined) => void
Moves focus to the next item.
-
previous
(unstable_allTheWay?: boolean | undefined) => void
Moves focus to the previous item.
-
up
(unstable_allTheWay?: boolean | undefined) => void
Moves focus to the item above.
-
down
(unstable_allTheWay?: boolean | undefined) => void
Moves focus to the item below.
#CompositeItemWidget
2 state props
These props are returned by the state hook. You can spread them into this component (
{...state}
) or pass them separately. You can also provide these props from your own state logic.
-
wrap
boolean | "horizontal" | "vertical"
Has effect only on two-dimensional composites. If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.
true
wraps between rows and columns.horizontal
wraps only between rows.vertical
wraps only between columns.- If
loop
matches the value ofwrap
, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
-
currentId
string | null | undefined
The current focused item
id
.undefined
will automatically focus the first enabled composite item.null
will focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentId
is initially set tonull
, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.