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_virtualprop 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 CompositeItems, you can do the following:
- Pass an
idprop to eachCompositeItem. - Memoize all non-primitive props that you're passing to
CompositeItem, including event handlers (e.g.onClick) and thechildrenprop.
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
virtualis set tofalse(default):CompositemItemhastabindexset to0if it's the current element. Otherwisetabindexis set to-1.
-
When
virtualis set totrue:Compositehastabindexset to0and hasaria-activedescendantset to the id of the currentCompositeItem.CompositeItemhasaria-selectedset totrueif it's the current element.
-
On one-dimensional composites:
- ↑ moves focus to the previous
CompositeItemiforientationisverticalor not defined. - ↓ moves focus to the next
CompositeItemiforientationisverticalor not defined. - → moves focus to the next
CompositeItemiforientationishorizontalor not defined. - ← moves focus to the previous
CompositeItemiforientationishorizontalor 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
CompositeItemabove. - ↓ moves focus to the
CompositeItembelow. - → moves focus to the next
CompositeItem. - ← moves focus to the previous
CompositeItem. - Home moves focus to the first
CompositeItemin the row. - End moves focus to the last
CompositeItemin the row. - PageUp moves focus to the first
CompositeItemin the column. - PageDown moves focus to the last
CompositeItemin the column. - Ctrl+Home moves focus to the first
CompositeItemin the composite element. - Ctrl+End moves focus to the last
CompositeItemin the composite element.
- ↑ moves focus to the
Learn more in Accessibility.
#Composition
Compositeuses Tabbable, and is used by TabList, RadioGroup, Menu and Toolbar.CompositeGroupuses Group and Id.CompositeItemuses Id and Clickable, and is used by Tab, Radio, MenuItem and ToolbarItem.
Learn more in Composition.
#Props
#useCompositeState
-
baseIdstringID that will serve as a base for all the items IDs.
-
unstable_virtualbooleanIf 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.
-
rtlbooleanDetermines how
nextandpreviousfunctions will behave. Ifrtlis 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" | undefinedDefines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the
orientationvalue 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.
-
currentIdstring | null | undefinedThe current focused item
id.undefinedwill automatically focus the first enabled composite item.nullwill focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentIdis initially set tonull, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
loopboolean | "horizontal" | "vertical"On one-dimensional composites:
trueloops from the last item to the first item and vice-versa.horizontalloops only iforientationishorizontalor not set.verticalloops only iforientationisverticalor not set.- If
currentIdis initially set tonull, the composite element will be focused in between the last and first items.
On two-dimensional composites:
trueloops 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.horizontalloops only from the last row item to the first item in the same row.verticalloops only from the last column item to the first item in the column row.- If
currentIdis 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
wrapmatches 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.
-
wrapboolean | "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.
truewraps between rows and columns.horizontalwraps only between rows.verticalwraps only between columns.- If
loopmatches 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.
-
shiftbooleanHas 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
-
disabledboolean | undefinedSame as the HTML attribute.
-
focusableboolean | undefinedWhen an element is
disabled, it may still befocusable. It works similarly toreadOnlyon form elements. In this case, onlyaria-disabledwill 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.
-
baseIdstringID that will serve as a base for all the items IDs.
-
unstable_virtualbooleanIf 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" | undefinedDefines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the
orientationvalue 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.
-
currentIdstring | null | undefinedThe current focused item
id.undefinedwill automatically focus the first enabled composite item.nullwill focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentIdis initially set tonull, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
wrapboolean | "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.
truewraps between rows and columns.horizontalwraps only between rows.verticalwraps only between columns.- If
loopmatches 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_movesnumberStores the number of moves that have been performed by calling
move,next,previous,up,down,firstorlast. -
groupsGroup[]Lists all the composite groups with their
idand DOMref. This state is automatically updated whenregisterGroupandunregisterGroupare called. -
itemsItem[]Lists all the composite items with their
id, DOMref,disabledstate andgroupIdif any. This state is automatically updated whenregisterItemandunregisterItemare called. -
setCurrentId(value: SetStateAction<string | null | undefine...Sets
currentId. This is different fromcomposite.moveas this only updates thecurrentIdstate without moving focus. When the composite widget gets focused by the user, the item referred by thecurrentIdstate will get focus. -
first() => voidMoves focus to the first item.
-
last() => voidMoves focus to the last item.
-
move(id: string | null) => voidMoves focus to a given item ID.
#CompositeGroup
-
idstring | undefinedSame 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.
-
baseIdstringID that will serve as a base for all the items IDs.
-
registerGroup(group: Group) => voidRegisters a composite group.
-
unregisterGroup(id: string) => voidUnregisters a composite group.
-
currentIdstring | null | undefinedThe current focused item
id.undefinedwill automatically focus the first enabled composite item.nullwill focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentIdis initially set tonull, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
unstable_movesnumberStores the number of moves that have been performed by calling
move,next,previous,up,down,firstorlast. -
itemsItem[]Lists all the composite items with their
id, DOMref,disabledstate andgroupIdif any. This state is automatically updated whenregisterItemandunregisterItemare called.
#CompositeItem
-
disabledboolean | undefinedSame as the HTML attribute.
-
focusableboolean | undefinedWhen an element is
disabled, it may still befocusable. It works similarly toreadOnlyon form elements. In this case, onlyaria-disabledwill be set. -
idstring | undefinedSame 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.
-
baseIdstringID that will serve as a base for all the items IDs.
-
unstable_virtualbooleanIf 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" | undefinedDefines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the
orientationvalue 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_movesnumberStores the number of moves that have been performed by calling
move,next,previous,up,down,firstorlast. -
currentIdstring | null | undefinedThe current focused item
id.undefinedwill automatically focus the first enabled composite item.nullwill focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentIdis initially set tonull, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.
-
itemsItem[]Lists all the composite items with their
id, DOMref,disabledstate andgroupIdif any. This state is automatically updated whenregisterItemandunregisterItemare called. -
setCurrentId(value: SetStateAction<string | null | undefine...Sets
currentId. This is different fromcomposite.moveas this only updates thecurrentIdstate without moving focus. When the composite widget gets focused by the user, the item referred by thecurrentIdstate will get focus. -
first() => voidMoves focus to the first item.
-
last() => voidMoves focus to the last item.
-
registerItem(item: Item) => voidRegisters a composite item.
-
unregisterItem(id: string) => voidUnregisters a composite item.
-
next(unstable_allTheWay?: boolean | undefined) => voidMoves focus to the next item.
-
previous(unstable_allTheWay?: boolean | undefined) => voidMoves focus to the previous item.
-
up(unstable_allTheWay?: boolean | undefined) => voidMoves focus to the item above.
-
down(unstable_allTheWay?: boolean | undefined) => voidMoves 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.
-
wrapboolean | "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.
truewraps between rows and columns.horizontalwraps only between rows.verticalwraps only between columns.- If
loopmatches 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.
-
currentIdstring | null | undefinedThe current focused item
id.undefinedwill automatically focus the first enabled composite item.nullwill focus the base composite element and users will be able to navigate out of it using arrow keys.- If
currentIdis initially set tonull, the base composite element itself will have focus and users will be able to navigate to it using arrow keys.