Skip to main content

Actions

Toolbar

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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then 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.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/toolbar

Its 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.10
import { 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 ToggleGroup instead for toggles that share one value (text alignment), ButtonGroup for a few joined buttons that are one control, and Menu with MenuBar for 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 (3000 is 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 with hidden. 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 has data-hidden on 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 a relative stage, but a stage that is itself absolutely placed (absolute inset-0) widens it past the stage, and a bar in a relative wrapper 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 with onHiddenChange={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 to false yourself 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 with true as the bar starts to fade out and false as it starts to come back, so your layout can move into its space and back. With hidden, 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 uses items: <Toolbar items={[...]}>.
  • area: Not a prop. The element whose pointer moves bring the bar back is autoHideArea: <Toolbar autoHide={3000} autoHideArea={stageRef}>.
  • autoHideDuration: Not a prop. autoHide takes the milliseconds: <Toolbar autoHide={3000}>.
  • children: Not a prop. The controls come from items: <Toolbar items={[{ label: "Bold", type: "toggle" }]}>.
  • glass: Not a prop. Use floating for 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, pass hidden with onHiddenChange: <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;
}