Toolbar
Buttons and toggles with one tab stop.
Example
Installation
pnpm dlx shadcn@latest add superzero11/uiset/toolbarIt lands in components/ui/toolbar.tsx, with UISet’s root (uiset). Wrap your app in <UISet> once, and every component takes its skin.
What UISet adds
The component is UI SDK’s own, with its parts, props and names, so everything below works as UI SDK documents it. UISet adds the look and checks it:
- Styles in five skins, each in light and dark, from CSS variables you can override.
- Text at 4.5:1 contrast and control edges and focus rings at 3:1, in every skin and theme, checked with axe in tests.
- Haptic cues on phones, and sound cues when you set
soundon<UISet>. - Examples that name every control, so agents can find it (see Agents below).
The API reference and accessibility notes below are UI SDK’s, from its docs for @ui-sdk/react (Apache-2.0). Toolbar on ui-sdk.dev has more usage examples. Your coding agent can read the same page at node_modules/@ui-sdk/react/docs/components/toolbar.md.
API Reference
Toolbar
orientation?(ToolbarOrientation): Which arrow keys move between items: left and right, or up and down.loop?(boolean): Whether arrow keys wrap from the last item to the first.disabled?(boolean): Stops every item from being used. Items stay focusable witharia-disabled, so the toolbar keeps its tab stop and stays discoverable.controller?({ focus: () => void; }): Focuses the toolbar from anywhere. Create one withcreateToolbarController().debug?(boolean): Logs every state change with its reason to the console, in development builds.
ToolbarButton
disabled?(boolean): Stops the item from being used. It stays focusable witharia-disabled, so arrow keys still reach it and a tooltip can say why.focusableWhenDisabled?(boolean): Whether a disabled item stays reachable with arrow keys. Turn it off to skip it, natively disabled.
ToolbarInput
disabled?(boolean): Stops the item from being used. It stays focusable witharia-disabled, so arrow keys still reach it and a tooltip can say why.focusableWhenDisabled?(boolean): Whether a disabled item stays reachable with arrow keys. Turn it off to skip it, natively disabled.
ToolbarGroup
disabled?(boolean): Disables every item in the group. They stay focusable witharia-disabled.
ToolbarLink and ToolbarSeparator take no props of their own.
State
What render functions and state-based class names receive on ToolbarButton and ToolbarInput:
disabled(boolean): Whether the item is disabled, by itself, its group or the toolbar.orientation(ToolbarOrientation): The toolbar'sorientationoption:"vertical", or"horizontal"for anything else.
On ToolbarLink:
disabled(boolean): Whether the link is disabled, by its group or the toolbar.orientation(ToolbarOrientation): The toolbar'sorientationoption:"vertical", or"horizontal"for anything else.
On ToolbarSeparator:
orientation(ToolbarOrientation): Which way the line runs: across the toolbar, so vertical in a horizontal toolbar.
Controller
focus()(() => void): Moves focus to the item that holds the toolbar's tab stop: the one focused last, or the first.
Accessibility
The toolbar follows the WAI-ARIA toolbar pattern. The root has the toolbar role and aria-orientation, and separators have the separator role.
Keyboard
The toolbar has one tab stop, a roving focus. Tab enters on the first enabled item, then returns to the item focused last. The next Tab leaves the toolbar.
| Key | Action |
|---|---|
ArrowRight / ArrowLeft | Move focus in a horizontal toolbar |
ArrowDown / ArrowUp | Move focus in a vertical toolbar |
Home / End | Focus the first or last item, except in a field |
Enter / Space | Activate a button or toggle, or open its popup |
Controls keep the keys they use: Down opens a menu, and Up and Down step a number input. Moving focus never presses a toggle. In right-to-left text, Left and Right swap.
Tested Behavior
Every line below is an automated test.
- The toolbar is named and has a horizontal orientation.
- Tab lands on the first item, and Tab again leaves the toolbar.
- Shift+Tab from after the toolbar lands on its tab stop.
- ArrowRight moves through every item, into toggle groups and out of fields.
- ArrowRight on the last item wraps to the first, and ArrowLeft back.
- Arrow keys never press a toggle, and Space and Enter do.
- Tab comes back to the item focused last.
- A toggle group in the toolbar is no tab stop of its own.
- A disabled button stays reachable, says it's disabled and does nothing.
- A button runs its action when pressed.
- A menu trigger rendered through a button opens with ArrowDown, and arrows still move past it.
- Choosing from a menu in the toolbar returns focus to its trigger.
- A text field keeps Home and End for its caret.
- Up and Down step a number field in the toolbar.
- Separators run across the toolbar.
- A group of buttons is named.
- A link in the toolbar follows its href with Enter.
- With loop off, arrows stop at the ends.
- A disabled toolbar keeps one tab stop, and every item says it's disabled.
Vertical
- ArrowDown and ArrowUp move between items, and Left and Right don't.
- Separators run across a vertical toolbar.
Disabled groups
- Toggles and toggle groups in a disabled group are disabled.
- A link in a disabled group is disabled, and follows its href once enabled.
- Enabling a group enables the toggles and buttons in it.
Runs in React, Vue and web components.
Agents
UI SDK’s agent bridge finds this component by its accessible name: give it a visible label or an aria-label, and an agent can read its state and call its actions without screenshots. Building with AI agents shows how to connect one.
This is agent.snapshot() for the example above, read live from this page. Change something in the example, then read it again: the values follow.
[
{
"ref": "c8",
"component": "toolbar",
"actions": [
"press"
],
"label": "Formatting",
"buttons": [
"Undo edit",
"Redo edit",
"Insert link"
]
}
]