A React hook that enables keyboard navigation within a container element. Implements the WAI-ARIA grid pattern for accessible keyboard navigation.
function useNavigation(options?: {
autofocus?: boolean;
rowLength?: number;
enabled?: boolean;
initialIndex?: number;
}): (root: HTMLElement) => void
| Option | Type | Default | Description |
|---|---|---|---|
| autofocus | boolean | false | Automatically focus the first item on mount |
| rowLength | number | 1 | Number of items per row (for grid layouts) |
| enabled | boolean | true | Enable/disable navigation |
| initialIndex | number | 0 | Item that receives initial focus |
import { useNavigation } from "aidos-ui";
function NavigableList() {
const rootRef = useNavigation();
return (
<ul ref={rootRef} aria-label="Options">
<li><button>Option 1</button></li>
<li><button>Option 2</button></li>
<li><button>Option 3</button></li>
</ul>
);
}
The hook manages tabIndex on focusable elements within the container:
Keyboard controls:
<List ariaLabel="example" navigation={true}>
<ListButtonItem headline="Headline 1" onClick={() => {}} />
<ListButtonItem headline="Headline 2" onClick={() => {}} />
<ListButtonItem headline="Headline 3" onClick={() => {}} />
</List>
<List ariaLabel="example" navigation={false}>
<ListButtonItem headline="Headline 1" onClick={() => {}} />
<ListButtonItem headline="Headline 2" onClick={() => {}} />
<ListButtonItem headline="Headline 3" onClick={() => {}} />
</List>
For grid layouts, set rowLength to enable proper up/down navigation:
function IconGrid() {
const rootRef = useNavigation({ rowLength: 4 });
return (
<div
ref={rootRef}
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
}}
>
{icons.map((icon) => (
<button key={icon.name} onClick={() => selectIcon(icon)}>
<Icon icon={icon.name} />
</button>
))}
</div>
);
}
With rowLength: 4:
Focus the first item automatically when the component mounts:
function DropdownMenu({ items }) {
const rootRef = useNavigation({ autofocus: true });
return (
<ul ref={rootRef} role="menu">
{items.map((item) => (
<li key={item.id} role="menuitem">
<button onClick={item.action}>{item.label}</button>
</li>
))}
</ul>
);
}
Enable or disable navigation dynamically:
function EditableList({ isEditing }) {
const rootRef = useNavigation({
enabled: !isEditing, // Disable during edit mode
});
return (
<ul ref={rootRef}>
{items.map((item) => (
<li key={item.id}>
{isEditing ? (
<TextInput value={item.name} />
) : (
<button>{item.name}</button>
)}
</li>
))}
</ul>
);
}
The hook uses a MutationObserver to handle items being added or removed:
function DynamicList() {
const [items, setItems] = useState(initialItems);
const rootRef = useNavigation();
const addItem = () => {
setItems([...items, { id: Date.now(), name: "New Item" }]);
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<ul ref={rootRef}>
{items.map((item) => (
<li key={item.id}>
<button>{item.name}</button>
</li>
))}
</ul>
</div>
);
}
This hook helps implement accessible keyboard navigation:
For best results, combine with proper ARIA attributes:
<ul
ref={rootRef}
role="listbox"
aria-label="Select an option"
>
<li role="option" aria-selected={selected === 1}>
<button>Option 1</button>
</li>
{/* ... */}
</ul>