Toolbar

A raised strip of controls — buttons, toggle groups, links, entry wells — with a single tab stop; arrow keys rove between items.

Tab once into the strip, then arrow between the toggles and buttons.

tsx
"use client"

import { Bold, Italic, Redo2, Underline, Undo2 } from "lucide-react"

import { Toggle } from "@/registry/seam/ui/toggle"
import { ToggleGroup } from "@/registry/seam/ui/toggle-group"
import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSeparator,
} from "@/registry/seam/ui/toolbar"

// One tab stop for the whole strip — arrow keys move between controls,
// including the embedded toggle group (its own well inside the raised strip).
export default function ToolbarDemo() {
  return (
    <Toolbar aria-label="Formatting">
      <ToggleGroup multiple aria-label="Text style">
        <Toggle value="bold" aria-label="Bold">
          <Bold />
        </Toggle>
        <Toggle value="italic" aria-label="Italic">
          <Italic />
        </Toggle>
        <Toggle value="underline" aria-label="Underline">
          <Underline />
        </Toggle>
      </ToggleGroup>
      <ToolbarSeparator />
      <ToolbarGroup aria-label="History">
        <ToolbarButton size="icon" className="size-9" aria-label="Undo">
          <Undo2 />
        </ToolbarButton>
        <ToolbarButton
          size="icon"
          className="size-9"
          aria-label="Redo"
          disabled
        >
          <Redo2 />
        </ToolbarButton>
      </ToolbarGroup>
    </Toolbar>
  )
}
bunx --bun @seamui/cli@latest add toolbar

API

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Layout and arrow-key axis; the separator flips automatically.
loopFocusbooleantrueArrow navigation wraps at the ends.
variant / sizebuttonVariants props"ghost" / "sm"On ToolbarButton — it wears the seam Button's cva.

Parts: Toolbar, ToolbarButton, ToolbarLink, ToolbarGroup, ToolbarSeparator, ToolbarInput. Embed a seam ToggleGroup directly — it joins the roving focus.

Notes

  • Base UI manages the roving focus through each item's ref, so ToolbarButton attaches press motion via the render prop (§5A Pattern A) instead of wrapping in the Button component — the wrapper would swallow the ref and kill arrow-key navigation.
  • The strip is a raised surface (shadow-resting); ToolbarInput is an entry well carved into it, and an embedded ToggleGroup keeps its own debossed well — keys on a key, wells in a key, per §1.
  • ToolbarButton presses with seam depth and taps the haptic on pointerdown; ToolbarLink stays a real link with no press depth, like pagination.

Press feedback, reduced motion, and haptics follow the global policy — see Motion and Haptics.