Toolbar

Surface for grouping icon-button actions with arrow-key roving focus

Installation

pnpm add @wandercom/design-system-web

Usage

28 lines
import { IconArrowDown } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconArrowDown';
import { IconArrowUp } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconArrowUp';
import { IconTrashCan } from '@central-icons-react/round-outlined-radius-2-stroke-1.5/IconTrashCan';
import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSeparator,
} from '@wandercom/design-system-web/ui/toolbar';

export function Example() {
  return (
    <Toolbar aria-label="Property card actions">
      <ToolbarGroup>
        <ToolbarButton aria-label="Move up">
          <IconArrowUp />
        </ToolbarButton>
        <ToolbarButton aria-label="Move down">
          <IconArrowDown />
        </ToolbarButton>
      </ToolbarGroup>
      <ToolbarSeparator />
      <ToolbarButton aria-label="Delete">
        <IconTrashCan />
      </ToolbarButton>
    </Toolbar>
  );
}

Examples

The default example shows the typical layout — two grouped actions, a separator, two more grouped actions, another separator, and a delete action. The floating variant (default) renders the toolbar as a blurred, elevated surface intended to hover over canvas-style content like cards or media.

Loading example...

Anatomy

Toolbar is built on Base UI's Toolbar primitive. The root provides the surface (defined per variant) and the role="toolbar" landmark with arrow-key roving focus. Inside, place any combination of:

  • ToolbarGroup — a logical cluster of related buttons (2px gap inside the cluster, 4px gap between siblings of the toolbar root). Wraps Toolbar.Group so a whole cluster can be disabled at once via the disabled prop.
  • ToolbarSeparator — a 1×16 hairline divider between groups, rendered as Toolbar.Separator.
  • ToolbarButton — a 32×32 ghost icon button rendered as Toolbar.Button, with hover, focus-visible, and pressed states.

The root accepts arbitrary children, so popup triggers, toggles, or other primitives can be dropped in alongside the standard subcomponents.

Composition

Use Base UI's render prop on ToolbarButton to delegate rendering to another primitive while keeping the toolbar's button styles. This is the recommended pattern when wrapping a Menu.Trigger, Popover.Trigger, or Dialog.Trigger.

6 lines
<Menu.Root>
  <ToolbarButton aria-label="More actions" render={<Menu.Trigger />}>
    <IconDotsHorizontal />
  </ToolbarButton>
  <Menu.Portal>{/* … */}</Menu.Portal>
</Menu.Root>

Tooltips work the other way around — Base UI's recommended pattern is to pass a ToolbarButton to Tooltip.Trigger's render prop:

6 lines
<Tooltip.Root>
  <Tooltip.Trigger render={<ToolbarButton aria-label="Delete" />}>
    <IconTrashCan />
  </Tooltip.Trigger>
  <Tooltip.Portal>{/* … */}</Tooltip.Portal>
</Tooltip.Root>

For toggle-style buttons, drive the visual state via aria-pressed (toggle semantics) or data-state="on" (when composed under a primitive that already manages state).

3 lines
<ToolbarButton aria-label="Edit" aria-pressed={isEditing}>
  <IconPencil />
</ToolbarButton>

Props

Toolbar

Plus all props from Base UI Toolbar.Root.

variant?:

'floating'
Visual treatment of the toolbar shell. Defaults to `floating` — a blurred, elevated surface for hovering over canvas-style content. Additional variants will be added as new surface treatments are introduced.

aria-label?:

string
Accessible label for the toolbar landmark. Defaults to "Toolbar". Override when the toolbar's purpose is more specific.

orientation?:

'horizontal' | 'vertical'
Orientation of the toolbar, used to set ARIA semantics and arrow-key roving focus direction. Defaults to `horizontal`.

loopFocus?:

boolean
Whether arrow-key navigation should loop from the last item back to the first. Defaults to `true`.

disabled?:

boolean
Disables every interactive item in the toolbar.

render?:

React.ReactElement | ((props, state) => React.ReactElement)
Replace the rendered element while keeping the toolbar behavior. Pass any element to use as the toolbar shell.

className?:

string
Additional CSS classes applied to the toolbar shell.

ToolbarGroup

Plus all props from Base UI Toolbar.Group.

disabled?:

boolean
Disables every interactive item inside the group at once.

render?:

React.ReactElement | ((props, state) => React.ReactElement)
Replace the rendered element while keeping the group behavior.

className?:

string
Additional CSS classes applied to the group wrapper.

ToolbarSeparator

Plus all props from Base UI Toolbar.Separator.

orientation?:

'horizontal' | 'vertical'
Override the separator orientation. Defaults to the inverse of the toolbar orientation.

render?:

React.ReactElement | ((props, state) => React.ReactElement)
Replace the rendered element while keeping the separator semantics.

className?:

string
Additional CSS classes applied to the separator.

ToolbarButton

Plus all props from Base UI Toolbar.Button.

aria-label:

string
Accessible name for the icon button. Required since the button is icon-only.

render?:

React.ReactElement | ((props, state) => React.ReactElement)
Replace the rendered element while keeping the toolbar button behavior. Use this to compose with other primitives such as `Menu.Trigger`, `Popover.Trigger`, or `Dialog.Trigger`.

aria-pressed?:

boolean
Toggle state. When true, the button shows the pressed background.

disabled?:

boolean
Disables the button and applies the disabled styling.

focusableWhenDisabled?:

boolean
Keep the button reachable via roving focus while disabled, instead of skipping it.

className?:

string
Additional CSS classes applied to the button.

Accessibility

  • The toolbar root renders as role="toolbar" with aria-orientation and an aria-label, so it is exposed as a landmark to assistive tech.
  • Roving focus is handled by Base UI: only one toolbar item is in the page tab sequence at a time, and arrow keys move between items (ArrowLeft/ArrowRight for horizontal, ArrowUp/ArrowDown for vertical). Home and End jump to the first and last items. Press Tab to leave the toolbar.
  • ToolbarGroup is a visual cluster — it controls the in-cluster gap (2px) versus the inter-cluster gap (4px) on the toolbar root, and forwards a disabled prop to every contained item.
  • ToolbarSeparator is rendered as Toolbar.Separator with role="separator" and an inverse orientation to the toolbar.
  • ToolbarButton is icon-only. Always pass an aria-label so screen readers can announce the action.
  • Pressed state can be expressed via either aria-pressed (toggle semantics) or data-state="on" (when composed under a primitive that drives state, like a Menu.Trigger). Both styles light up the same pressed background.
  • focus-visible shows a 2px ring rendered with a 1px gap from the button edge, so the indicator stays legible against both the toolbar surface and the underlying canvas.

Edge cases

  • The toolbar is w-fit — it never stretches to fill its container. Wrap it in a positioning element when overlaying canvas content.
  • Disabled items are skipped by the roving focus by default. Pass focusableWhenDisabled on ToolbarButton to keep them reachable so screen readers can announce the disabled state.
  • When using render to swap in another primitive's trigger, that primitive owns the underlying element type (button, anchor, etc.) — Base UI merges the toolbar button's props and refs onto it.
Toolbar