A row of controls for one surface, such as an editor's formatting: Button commands and Toggle states, in groups with separators. It is one tab stop, and the arrow keys, Home and End move between its controls (the WAI-ARIA toolbar pattern). Icon-only controls show their label, and any shortcut as Kbd, in a tooltip. When the row is too narrow, the controls that do not fit move into a More menu at its end, toggles as checkbox items.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/toolbar#v1.3.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/toolbarIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/toolbar#v1.3.10 ourostack/ouro-ui/toolbar-tests#v1.3.10import { Toolbar } from "@/components/ouro/toolbar/toolbar";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Toolbar
label="Formatting"
keepFocus
items={[
{ type: "toggle", label: "Bold", icon: BoldIcon, hideLabel: true, shortcut: ["mod", "b"], pressed: bold, onPressedChange: setBold },
{ type: "toggle", label: "Italic", icon: ItalicIcon, hideLabel: true, shortcut: ["mod", "i"], pressed: italic, onPressedChange: setItalic },
{ type: "separator" },
{ label: "Insert link", icon: LinkIcon, hideLabel: true, shortcut: ["mod", "k"], onClick: insertLink },
]}
/>
// Show the bar when the person uses the app's call shortcuts (⇧⌘M mutes, Ctrl+Shift+M off
// Apple platforms), and never for other keys, such as typing in the chat.
const [hidden, setHidden] = useState(false);
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (!((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === "m")) return;
toggleMute();
setHidden(false);
};
addEventListener("keydown", onKey);
return () => removeEventListener("keydown", onKey);
}, []);
<Toolbar
label="Call controls"
floating
autoHide={3000}
hidden={hidden}
onHiddenChange={setHidden}
items={[{ type: "toggle", label: "Mute", icon: MicOffIcon, hideLabel: true, shortcut: ["shift", "mod", "m"], pressed: muted, onPressedChange: toggleMute }]}
/>Usage
keepFocus leaves focus in a text editor when a control is pressed with a mouse or a finger, so its selection stays.
floating draws the bar as glass over content, such as a calling app's controls over video, and autoHide fades it out while the pointer rests, until the pointer moves over the stage or focus enters it: <Toolbar label="Call controls" floating autoHide={3000} items={controls} />, in a wrapper your layout places over the video (pointer-events-none absolute inset-x-0 bottom-4 flex justify-center inside the relative stage, so the wrapper does not take presses meant for the pictures beside the bar; a floating bar takes its own). autoHideArea names the stage when the layout cannot, and onHiddenChange tells your layout when the bar fades and comes back. Keys pressed outside the bar leave it faded, so typing in a chat leaves it faded too; to show it when the person uses your app's own call shortcuts, control it with hidden, as the second example does.
Its agent side is its controls'. Toolbar is a container with nothing of its own for an agent to read or do, so it declares no agent side; the controls inside declare their own, and a calling app declares its call controls once, in the component that holds them, not in the bar. Give each control the id of its action with axAction, which the control and its More-menu item carry as data-ax-action, so that component's declaration finds its controls.
Use instead when
- Use
ToggleGroupinstead for toggles that share one value (text alignment),ButtonGroupfor a few joined buttons that are one control, andMenuwithMenuBarfor an application's menus.
Props
Type: ToolbarProps.
autoHide- Type:
number | (boolean & { 'Pass the milliseconds of rest: autoHide={3000}': never }) - Fades the bar out after this many milliseconds of pointer rest, as a calling app's controls fade over video (
3000is usual). It fades with opacity alone, over the theme's base duration with its ease-out, the same with reduced motion, and its tooltips close as it goes. A pointer move over its area (autoHideArea) brings it back, and so do focus entering it, from Tab, a screen reader or Voice Control, and a key pressed in it. Keys pressed elsewhere, such as typing in a chat or holding Space to talk, leave it as it is; to show it for your app's own shortcuts, control it withhidden. It never fades while the pointer is over it, while its More menu is open, while a dialog, popover or other top-layer element that one of its controls opened is open, such as a share picker, or while keyboard focus is inside it, until the pointer moves on its area. A control's tooltip holds it only as the pointer or keyboard focus does, so a tooltip that Tab showed lets it go once the pointer moves. A control pressed with the mouse keeps no hold, so the bar still fades after a click on Mute. While faded it stays in the accessibility tree and focusable, so assistive technology still reaches every control; it only takes no pointer presses, drops its blur, and hasdata-hiddenon the root. autoHideArea- Type:
Element | RefObject<Element | null> | null - With
autoHide: the element whose pointer moves bring the bar back, such as the call's stage, as an element or a ref. Without it, the bar finds the element it floats over from the layout: its offset parent (its nearest positioned ancestor), or, when that is an absolutely placed or fixed wrapper around the bar, the wrapper's own, and so on up. That default needs nothing wired up for a bar in a wrapper on arelativestage, but a stage that is itself absolutely placed (absolute inset-0) widens it past the stage, and a bar in arelativewrapper narrows it to the wrapper: pass the stage then. A bar with no positioned ancestor, or in a fixed wrapper, listens to the whole page. className- Type:
string - Layout only: margins, width, flex and grid placement (
flex-1,mt-2). Appearance classes warn in development. disabled- Type:
boolean - Disables every control. They stay reachable with the arrow keys.
floating- Type:
boolean - Draws the bar as glass that floats over content, as a calling app's controls float over video: a translucent surface that blurs what is behind it, with a hairline edge, a soft shadow and rounded corners, all from the theme. It turns solid, with no blur, when the system asks for reduced transparency, and solid with a defined edge when it asks for more contrast. It is the look only: where the bar floats stays your layout, such as an absolutely placed wrapper over the stage.
hidden- Type:
boolean - With
autoHide: whether the bar is faded, when your app controls it. Pair it withonHiddenChange={setHidden}, which asks for each fade and return, so the bar still fades after a rest and comes back on a pointer move. Set it tofalseyourself to show the bar for something only your app knows about, such as the person using your app's call shortcuts; a new rest starts then. Keys anywhere else, such as typing in a chat, still never show it. Without it, the bar keeps its own state. items(required)- Type:
readonly ToolbarEntry[] - The controls, in order: buttons, toggles, groups of them and separators.
keepFocus- Type:
boolean - A press with a mouse or a finger leaves focus where it was, so a text editor beside the toolbar keeps its caret and selection. With nothing focused, the control takes focus as usual. The keyboard still reaches the toolbar with Tab.
label(required)- Type:
string - Names the toolbar for screen readers ("Formatting"). It is not shown.
moreLabel- Type:
string - Default:
"More" - The name of the button that opens the controls that do not fit, and its tooltip.
onHiddenChange- Type:
((hidden: boolean) => void) - With
autoHide: called withtrueas the bar starts to fade out andfalseas it starts to come back, so your layout can move into its space and back. Withhidden, it is how the bar asks, and it fades or comes back once you pass the new value. ref- Type:
Ref<HTMLDivElement> - The toolbar element (
role="toolbar"). size- Type:
"sm" | "md" | "lg" - Default:
"md" - Every control's size, as Button's and Toggle's.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
actions: Not a prop. Ouro UI usesitems:<Toolbar items={[...]}>.area: Not a prop. The element whose pointer moves bring the bar back isautoHideArea:<Toolbar autoHide={3000} autoHideArea={stageRef}>.autoHideDuration: Not a prop.autoHidetakes the milliseconds:<Toolbar autoHide={3000}>.children: Not a prop. The controls come fromitems:<Toolbar items={[{ label: "Bold", type: "toggle" }]}>.glass: Not a prop. Usefloatingfor the glass look:<Toolbar floating>.orientation: Not a prop. A Toolbar is a row; controls that do not fit move into its More menu.visible: Not a prop. To control whether the bar shows, passhiddenwithonHiddenChange:<Toolbar autoHide={3000} hidden={hidden} onHiddenChange={setHidden}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2).
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ToolbarButtonEntry
A toolbar entry: a command, such as Insert link. The default type.
interface ToolbarButtonEntry extends ToolbarControlEntry {
type?: "button" | undefined;
/** Runs when the control is pressed, or chosen from the overflow menu. */
onClick?: (() => void) | undefined;
}ToolbarEntry
One entry in a Toolbar's items: a button (the default type), a toggle, a group or a separator.
type ToolbarEntry = ToolbarButtonEntry | ToolbarToggleEntry | ToolbarGroupEntry | { type: "separator" };ToolbarGroupEntry
A toolbar entry: related controls under a name screen readers hear ("Lists").
interface ToolbarGroupEntry {
type: "group";
/** Names the group for screen readers, and labels it in the overflow menu. */
label: string;
items: readonly (ToolbarButtonEntry | ToolbarToggleEntry)[];
}ToolbarToggleEntry
A toolbar entry: an on/off state, such as Bold, drawn as a Toggle (aria-pressed).
interface ToolbarToggleEntry extends ToolbarControlEntry {
type: "toggle";
/** Whether it is on, when you control it. Pair it with `onPressedChange`. */
pressed?: boolean | undefined;
/** Whether it starts on, when the toolbar keeps its state. @default false */
defaultPressed?: boolean | undefined;
/** Called with the new state when it is pressed, or chosen from the overflow menu. */
onPressedChange?: ((pressed: boolean) => void) | undefined;
}