Photon Design System

Toolbar

A row of related controls that acts as a single tab stop, with arrow-key navigation between items — formatting bars, editor controls. Built on Base UI Toolbar.

Import

import { Toolbar } from "@photon-ai/pho-ui/components/toolbar";

Basic

Toolbar.Button, Link, and Input are the controls; Toolbar.Separator divides groups. Every icon-only control needs an aria-label.

<Toolbar.Root aria-label="Formatting">
  <Toolbar.Button aria-label="Bold">
    <TextB weight="bold" />
  </Toolbar.Button>
  <Toolbar.Button aria-label="Italic">
    <TextItalic />
  </Toolbar.Button>
  <Toolbar.Separator />
  <Toolbar.Button aria-label="Align left">
    <TextAlignLeft />
  </Toolbar.Button>
</Toolbar.Root>

Pressable toggles

A plain Toolbar.Button fires an action; for a formatting control that stays on until pressed again, render a Toggle through it with render. The button keeps its place in the roving focus order while Toggle tracks the pressed state (data-[pressed]). Group related toggles with Toolbar.Group.

import { Toggle } from "@photon-ai/pho-ui/components/toggle";

<Toolbar.Root aria-label="Formatting">
  <Toolbar.Group>
    <Toolbar.Button render={<Toggle aria-label="Bold" />}>
      <TextB weight="bold" />
    </Toolbar.Button>
    <Toolbar.Button render={<Toggle aria-label="Italic" defaultPressed />}>
      <TextItalic />
    </Toolbar.Button>
  </Toolbar.Group>
</Toolbar.Root>;

Toolbar.Input and Toolbar.Link join the same roving focus order as buttons, so a search field or a jump-out link stays reachable with the arrow keys.

<Toolbar.Root aria-label="Editor">
  <Toolbar.Button aria-label="Undo">
    <ArrowUUpLeft />
  </Toolbar.Button>
  <Toolbar.Separator />
  <Toolbar.Input placeholder="Search" aria-label="Search" />
  <Toolbar.Separator />
  <Toolbar.Link href="/docs">Docs</Toolbar.Link>
</Toolbar.Root>

Anatomy

  • Toolbar.Root — the container; one tab stop, arrow keys move between items
  • Toolbar.Button — a button control (or a trigger for Toggle / a menu via render)
  • Toolbar.Link — an <a> control in the same focus order
  • Toolbar.Input — a text field in the same focus order
  • Toolbar.Group — a sub-group of related controls
  • Toolbar.Separator — a divider between groups (vertical in a horizontal toolbar)

Props

Toolbar.Root (renders a <div role="toolbar">)

  • orientation — horizontal · vertical (default horizontal); sets the arrow-key axis
  • loopFocus — boolean (default true) — wrap focus from the last item back to the first
  • disabled — boolean (default false) — disable every control in the toolbar

Toolbar.Button (renders a <button>)

  • disabled — boolean (default false) — disable this control
  • focusableWhenDisabled — boolean (default true) — keep it focusable while disabled so it’s still announced
  • render — render as another element (e.g. a Toggle or menu trigger)

Toolbar.Separator (renders a <div role="separator">)

  • orientation — defaults to the opposite of the toolbar’s orientation

Accessibility

Toolbar.Root renders role="toolbar" and reflects aria-orientation. Give it an aria-label (or aria-labelledby) so its purpose is announced, and give every icon-only Toolbar.Button an aria-label that names the action.

  • Tab — move focus onto the toolbar (a single tab stop) and off it again
  • Arrow keys — move between items — Left/Right when horizontal, Up/Down when vertical

Roving tabindex keeps only one item tabbable, so the whole toolbar is one stop in the page’s tab order. Focus wraps at the ends while loopFocus is true. Disabled buttons remain focusable by default (focusableWhenDisabled) so screen readers still announce them.

Best practices

  • Reach for a toolbar when several controls act on one target and should share a single tab stop — it keeps keyboard traversal quick.
  • Label the toolbar (aria-label) and every icon-only button so the purpose is announced.
  • Separate semantic groups (formatting vs alignment) with Toolbar.Separator instead of relying on spacing alone.
  • Use Toolbar.Button for actions and a Toggle rendered through it for on/off formatting states — don’t fake a pressed state with styling.