useRovingFocus
Give a composite widget (toolbar, tab list, radio group, grid) a single tab stop. Arrow keys, Home/End and optional typeahead move focus between its items.
Tab reaches the toolbar once. ←/→ move between the buttons (wrapping at the ends, and swapped under RTL), Home/End jump to the first and last, and Tab leaves the group.
useRovingFocus({ count }) returns getItemProps(i) (tabIndex, ref, onKeyDown, onFocus) to spread on each item, plus activeIndex, focusItem and handleKeyDown for items that route keys themselves. Other options: orientation ('vertical', or 'both' with columns for a grid where ↑/↓ move by a row), loop, controlled activeIndex / onActiveChange, isDisabled to skip items, and typeahead: (i) => label to jump to an item by typing its first letters. Keys and DOM focus are web-only; on native the item props are inert apart from tabIndex.Loading demo…
import { useState } from 'react';
import { Block, IconButton, a11yProps, useRovingFocus } from '@plocks/ui';
const TOOLS = [
{ icon: 'bold', label: 'Bold' },
{ icon: 'italic', label: 'Italic' },
{ icon: 'underline', label: 'Underline' },
{ icon: 'strikethrough', label: 'Strikethrough' },
{ icon: 'code', label: 'Code' },
];
export function Demo() {
const [active, setActive] = useState<string[]>(['Bold']);
const { getItemProps } = useRovingFocus({ count: TOOLS.length });
const toggle = (label: string) =>
setActive((current) =>
current.includes(label) ? current.filter((item) => item !== label) : [...current, label]
);
return (
<Block
direction="row"
gap="xs"
p="xs"
radius="md"
bg="subtle"
{...a11yProps({ role: 'toolbar', label: 'Text formatting', orientation: 'horizontal' })}
>
{TOOLS.map((tool, index) => {
const on = active.includes(tool.label);
return (
<IconButton
key={tool.label}
icon={tool.icon}
variant={on ? 'filled' : 'ghost'}
accessibilityLabel={tool.label}
onPress={() => toggle(tool.label)}
{...getItemProps(index)}
{...a11yProps({ pressed: on })}
/>
);
})}
</Block>
);
}React Native design for iOS, Android, and Web.
Quick Links
Resources