Add this component

npx zentauri-ui add toolbar

Accessibility notes

Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Navigation

Toolbar for related actions and formatting

Compose action buttons, formatting toggles, links, and groups in a toolbar with one keyboard entry point. Explore horizontal and vertical layouts, RTL navigation, wrapping, and 26 themeable appearances.

Ready to work.

Toolbar playground

Explore orientation, direction, wrapping, disabled actions, and every appearance. Try the keyboard directly in the preview.

Tab enters the toolbar once. Use Left/Right for horizontal layouts and Up/Down for vertical layouts. Home/End jump to the edges. Wrapping keeps controls visible on narrow screens; navigation follows DOM order across rows.


No action yet. Bold is off.

Appearance gallery

Toolbar recipes

Document actions and feedback


Ready to work.

Controlled formatting toggles


A small change can make an idea clearer.

Vertical canvas tools


Select a canvas tool.

Existing buttons, links, and disabled actions


Nothing selected yet.

Tab enters once. Arrows move between available controls; Home and End jump to the edges.

Composition and keyboard behavior

Give every Toolbar an aria-label or aria-labelledby that describes its purpose. Use it for groups of at least three related controls. Label icon-only actions, and use aria-controls when the commands operate on a specific editor or canvas.

After hydration, the first available managed item is the only Tab stop. Arrow navigation and pointer focus update that entry point, so returning with Tab restores the last focused item. Home and End jump to the first and last available controls. Horizontal Left/Right follow the reading direction; vertical Up/Down stay the same in RTL. Set loop=false to stop at either edge.

ToolbarButton preserves native button props and defaults to type=button. ToolbarToggle supports pressed, defaultPressed, and onPressedChange; navigation moves focus without changing its state. ToolbarLink is a native anchor. ToolbarGroup groups related actions, and ToolbarSeparator is decorative unless decorative=false.

ToolbarItem composes one focusable child that forwards props and ref to its DOM control. It preserves the child’s handlers, merges classes and refs, and adds it to the toolbar’s navigation. Use native=false for links or controls that implement their own disabled behavior. Prevented events are respected. Popup content and nested toolbars keep their own navigation; perpendicular keys remain available to menu triggers.

Native disabled, aria-disabled, hidden, and inert managed items are skipped. Root disabled disables the complete toolbar, and disabled links cannot activate. Dynamic additions, removals, and visibility changes update the Tab stop. Native text editing keys remain with the input; prefer keeping text inputs outside the toolbar’s arrow-navigation sequence.

Wrapping is enabled by default for narrow screens. With wrap=false, provide a scrolling container when controls exceed the available width. Arrow navigation follows DOM order across wrapped rows. The static entry has no animation dependency and respects reduced motion for its CSS color transitions.

WAI toolbar accessibility guidance

Toolbar API

Generated from the package prop types and variant definitions.

Toolbar

ToolbarProps

Variants

PropTypeDefault
appearance
bluecontrastcyandefaultemeraldglassgradient-bluegradient-greengradient-orangegradient-pinkgradient-purplegray+14
default
orientation
horizontalvertical
horizontal
size
lgmdsm
md
wrap
falsetrue
true

Behavior

PropTypeDefault
disabledDisable all managed items. Native disabled items are skipped.boolean | undefinednone
loopWrap arrow navigation at the first and last items. Defaults to true.boolean | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

ToolbarButton

ToolbarButtonProps

Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLButtonElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

ToolbarGroup

ToolbarGroupProps

Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

ToolbarItem

ToolbarItemProps

Behavior

PropTypeDefault
disabledboolean | undefinednone
nativeForward native disabled to button-like controls. Set false for links/custom widgets.boolean | undefinednone

Content

PropTypeDefault
children*One focusable element that forwards props and ref to its DOM control.ReactElement<unknown, string | JSXElementConstructor<any>>none
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

ToolbarLink

ToolbarLinkProps

Behavior

PropTypeDefault
disabledboolean | undefinednone
href*stringnone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLAnchorElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

ToolbarSeparator

ToolbarSeparatorProps

Behavior

PropTypeDefault
decorativeDecorative by default; false exposes separator semantics and orientation.boolean | undefinednone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

ToolbarToggle

ToolbarToggleProps

Behavior

PropTypeDefault
defaultPressedInitial uncontrolled pressed state. Defaults to false.boolean | undefinednone
onPressedChange((pressed: boolean) => void) | undefinednone
pressedControlled pressed state. Arrow navigation never changes it.boolean | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLButtonElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

Installation and theme overrides

pnpm add @zentauri-ui/zentauri-components
# Or vendor the Toolbar and the Button used in the composition recipe:
pnpm dlx @zentauri-ui/zentauri-components add toolbar buttons

Include the package in Tailwind v4 scan scope. Adjust the source path for your stylesheet; vendored files also need scanning.

@import "tailwindcss";
@source "../node_modules/@zentauri-ui/zentauri-components";

.editor-toolbar {
  --zui-toolbar-radius: 1rem;
  --zui-toolbar-radius-dark: 1rem;
  --zui-toolbar-gap: 0.5rem;
  --zui-toolbar-gap-dark: 0.5rem;
  --zui-toolbar-focus-ring: #7c3aed;
  --zui-toolbar-focus-ring-dark: #c4b5fd;
}

Apply className="editor-toolbar" to Toolbar. Its appearance provides the surface and inherited foreground; each item uses Toolbar spacing, hover, pressed, and focus tokens. A composed Button can keep its own appearance via class merging.

Browse all Toolbar light and dark tokens
CSS variable overrides

Toolbar CSS variables

Theme toolbar surfaces, action and pressed states, sizes, focus indicators, groups, and separators. Every light token has a dark counterpart.

220 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-toolbar-radius: 0.75rem;
  --zui-toolbar-border-width: 1px;
  --zui-toolbar-padding: 0.375rem;
  --zui-toolbar-gap: 0.25rem;
  --zui-toolbar-item-radius: 0.5rem;
  --zui-toolbar-item-gap: 0.375rem;
  --zui-toolbar-item-bg: transparent;
  --zui-toolbar-item-hover-bg: #64748b1a;
  --zui-toolbar-item-pressed-bg: #64748b33;
  --zui-toolbar-disabled-opacity: 0.5;
  --zui-toolbar-focus-width: 2px;
  --zui-toolbar-focus-offset: 2px;
  --zui-toolbar-focus-ring: currentColor;
  --zui-toolbar-item-height-sm: 2rem;
  --zui-toolbar-item-padding-sm: 0.5rem;
  --zui-toolbar-font-size-sm: 0.75rem;
  --zui-toolbar-item-height-md: 2.25rem;
  --zui-toolbar-item-padding-md: 0.75rem;
  --zui-toolbar-font-size-md: 0.875rem;
  --zui-toolbar-item-height-lg: 2.75rem;
  --zui-toolbar-item-padding-lg: 1rem;
  --zui-toolbar-font-size-lg: 1rem;
  --zui-toolbar-group-gap: 0.25rem;
  --zui-toolbar-separator-fg: #64748b66;
  --zui-toolbar-separator-width: 1px;
  --zui-toolbar-separator-inset: 0.25rem;
  --zui-toolbar-default-bg: #ffffff;
  --zui-toolbar-default-fg: #0f172a;
  --zui-toolbar-default-border: #e2e8f0;
  --zui-toolbar-subtle-bg: #f8fafc;
  --zui-toolbar-subtle-fg: #334155;
  --zui-toolbar-subtle-border: #e2e8f0;
  --zui-toolbar-contrast-bg: #0f172a;
  --zui-toolbar-contrast-fg: #f8fafc;
  --zui-toolbar-contrast-border: #334155;
  --zui-toolbar-glass-bg: #ffffffb3;
  --zui-toolbar-glass-fg: #0f172a;
  --zui-toolbar-glass-border: #cbd5e1;
  --zui-toolbar-glass-blur: 12px;
  --zui-toolbar-blue-bg: #eff6ff;
  --zui-toolbar-blue-fg: #1e40af;
  --zui-toolbar-blue-border: #bfdbfe;
  --zui-toolbar-cyan-bg: #ecfeff;
  --zui-toolbar-cyan-fg: #155e75;
  --zui-toolbar-cyan-border: #a5f3fc;
  --zui-toolbar-green-bg: #f0fdf4;
  --zui-toolbar-green-fg: #166534;
  --zui-toolbar-green-border: #bbf7d0;
  --zui-toolbar-lime-bg: #f7fee7;
  --zui-toolbar-lime-fg: #3f6212;
  --zui-toolbar-lime-border: #d9f99d;
  --zui-toolbar-emerald-bg: #ecfdf5;
  --zui-toolbar-emerald-fg: #065f46;
  --zui-toolbar-emerald-border: #a7f3d0;
  --zui-toolbar-indigo-bg: #eef2ff;
  --zui-toolbar-indigo-fg: #3730a3;
  --zui-toolbar-indigo-border: #c7d2fe;
  --zui-toolbar-purple-bg: #faf5ff;
  --zui-toolbar-purple-fg: #6b21a8;
  --zui-toolbar-purple-border: #e9d5ff;
  --zui-toolbar-pink-bg: #fdf2f8;
  --zui-toolbar-pink-fg: #9d174d;
  --zui-toolbar-pink-border: #fbcfe8;
  --zui-toolbar-rose-bg: #fff1f2;
  --zui-toolbar-rose-fg: #9f1239;
  --zui-toolbar-rose-border: #fecdd3;
  --zui-toolbar-sky-bg: #f0f9ff;
  --zui-toolbar-sky-fg: #075985;
  --zui-toolbar-sky-border: #bae6fd;
  --zui-toolbar-teal-bg: #f0fdfa;
  --zui-toolbar-teal-fg: #115e59;
  --zui-toolbar-teal-border: #99f6e4;
  --zui-toolbar-yellow-bg: #fefce8;
  --zui-toolbar-yellow-fg: #854d0e;
  --zui-toolbar-yellow-border: #fef08a;
  --zui-toolbar-orange-bg: #fff7ed;
  --zui-toolbar-orange-fg: #9a3412;
  --zui-toolbar-orange-border: #fed7aa;
  --zui-toolbar-red-bg: #fef2f2;
  --zui-toolbar-red-fg: #991b1b;
  --zui-toolbar-red-border: #fecaca;
  --zui-toolbar-slate-bg: #f8fafc;
  --zui-toolbar-slate-fg: #334155;
  --zui-toolbar-slate-border: #cbd5e1;
  --zui-toolbar-gray-bg: #f9fafb;
  --zui-toolbar-gray-fg: #374151;
  --zui-toolbar-gray-border: #d1d5db;
  --zui-toolbar-zinc-bg: #fafafa;
  --zui-toolbar-zinc-fg: #3f3f46;
  --zui-toolbar-zinc-border: #d4d4d8;
  --zui-toolbar-gradient-blue-from: #eff6ff;
  --zui-toolbar-gradient-blue-to: #faf5ff;
  --zui-toolbar-gradient-blue-fg: #0f172a;
  --zui-toolbar-gradient-blue-border: #bfdbfe;
  --zui-toolbar-gradient-green-from: #f0fdf4;
  --zui-toolbar-gradient-green-to: #f7fee7;
  --zui-toolbar-gradient-green-fg: #0f172a;
  --zui-toolbar-gradient-green-border: #bbf7d0;
  --zui-toolbar-gradient-purple-from: #faf5ff;
  --zui-toolbar-gradient-purple-to: #fdf2f8;
  --zui-toolbar-gradient-purple-fg: #0f172a;
  --zui-toolbar-gradient-purple-border: #e9d5ff;
  --zui-toolbar-gradient-orange-from: #fff7ed;
  --zui-toolbar-gradient-orange-to: #fefce8;
  --zui-toolbar-gradient-orange-fg: #0f172a;
  --zui-toolbar-gradient-orange-border: #fed7aa;
  --zui-toolbar-gradient-pink-from: #fdf2f8;
  --zui-toolbar-gradient-pink-to: #fff1f2;
  --zui-toolbar-gradient-pink-fg: #0f172a;
  --zui-toolbar-gradient-pink-border: #fbcfe8;
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-toolbar-radius-dark: 0.75rem;
  --zui-toolbar-border-width-dark: 1px;
  /* ...same variables with -dark at the end */
}

What it does

Groups related commands with button, toggle, link, item, group, and separator primitives. Supports 26 appearances, three sizes, and paired theme tokens.

Common use cases

  • Editor formatting and document actions.
  • Canvas tools and dashboard commands.
  • Related links and actions in workspaces.

Accessibility

Name each toolbar, label icon-only controls, and group at least three related actions. Native buttons and links preserve activation semantics, while roving tabindex reduces repeated Tab stops. Prevented events and nested widget navigation are respected.

FAQ

How does keyboard navigation work?

After hydration, Tab enters at the first available item, then returns to the most recently focused item. Horizontal arrows follow the reading direction; vertical toolbars use Up/Down. Home and End jump to the edges, and loop defaults to true.

Can I compose an existing Button?

Yes. Wrap a single focusable control in ToolbarItem. It must forward props and ref to its DOM element. For links or non-native widgets, set native=false and implement their disabled semantics.

Do arrows activate toggle buttons?

No. Arrows only move focus. Click, Enter, or Space activate ToolbarToggle. Use pressed and onPressedChange for controlled state, or defaultPressed for local state.

How does Toolbar handle disabled and hidden items?

Disabled, aria-disabled, hidden, and inert managed items are skipped. Root disabled disables all managed actions and blocks link activation. Changes to items update the keyboard entry point.

What happens on mobile?

Controls wrap by default. Set wrap=false and provide a scrolling container when you want a single row. Navigation follows DOM order across rows.