Toolbar
A keyboard-navigable band that groups icon controls, dropdown triggers, toggle sets and separators into one unit. Built on Radix Toolbar, so it ships roving tab focus and arrow-key movement.
Installation#
Usage#
import {
Toolbar,
ToolbarButton,
ToolbarSeparator,
ToolbarToggleGroup,
ToolbarToggleItem,
} from "@/components/ui/toolbar"
export function Example() {
const [marks, setMarks] = useState<string[]>(["bold"])
return (
<Toolbar aria-label="Formatting">
<ToolbarToggleGroup type="multiple" value={marks} onValueChange={setMarks}>
<ToolbarToggleItem value="bold" tooltip="Bold" shortcut="⌘B">
<TextB />
</ToolbarToggleItem>
<ToolbarToggleItem value="italic" tooltip="Italic" shortcut="⌘I">
<TextItalic />
</ToolbarToggleItem>
</ToolbarToggleGroup>
<ToolbarSeparator />
<ToolbarButton tooltip="Link" shortcut="⌘K">
<LinkSimple />
</ToolbarButton>
</Toolbar>
)
}Collapsed#
A full format bar can outgrow its surface. Collapse it: give each cluster its own caret dropdown that unlocks only its options (text style, color, marks, link, list, alignment, insert). The same bar, a fraction of the width, and every trigger is the same affordance: a caret, one meaning. (The three-dots overflow is a different thing entirely, reserved for the automatic Responsive overflow below, where it means "controls that didn't fit.") Any ToolbarButton used as a dropdown trigger stays lit while its menu is open, and a caret button flips its chevron. No extra wiring: it's built into the button.
Responsive overflow#
Let the bar collapse itself. ToolbarOverflow takes your controls as data, shows as many as fit, and folds the rest into the three-dots menu, live, expanding again when the space returns. Drag the handle to shrink the track and watch controls move into (and back out of) More. Each control's footprint is measured once on first paint, then a ResizeObserver only re-runs a cheap greedy pack, so resizing stays smooth. Each control is declared once and rendered in both places (a bar button and a menu row), so the two never drift.
Scrolling#
The low-chrome alternative to an overflow menu: pass scrollable and the bar scrolls on its axis instead of wrapping when its controls exceed the width. A scroll-aware edge fade (the scroll-fade utility) hints that there's more on whichever side you can still scroll toward, and the scrollbar is hidden. Reach for this when every control should stay reachable without a menu (a mobile format bar).
Variants#
Three levels of chrome. floating (the default) is an elevated pill that hovers over content, like an on-selection format bar. outline is the same shape sitting flat on the page, and plain is chromeless for a header or footer that already owns its surface.
Solid#
solid is an inverted, high-contrast strip for a contextual bar sitting over media or imagery: a player overlay, a canvas HUD. It's token-pure, flipping foreground and background, so it reads dark on light themes and light on dark, and re-tints its controls and dividers to match.
Primary action#
When a bar exists for one action, play in a transport or run in an editor, mark that control with variant="primary". It fills with the brand accent, like a primary Button, and keeps its siblings' box and corners, so the band's rhythm and concentric radius hold and only the fill says it leads. Hover, pressed and open states stay on the fill. Keep it to one per bar; a second filled control and neither one leads.
Design canvas#
The same shell dressed as a design tool's canvas HUD, the way Figma floats its bar over the artboard. It's the solid variant for the black chrome, a full run of canvas tools (move, frame, shapes, pen, text, hand, comment, insert), and a trailing group for AI and Dev Mode. Tools are single-select: the one in hand flips to a brand-blue chip, matching Figma's selected tool and the DS's brand-for-primary rule, while shapes and the pen are caret dropdowns whose trigger mirrors the variant currently in hand.
Sizes#
sm (24px controls) for dense rails, md (28px, the default), and lg (36px) for a touch-friendly or marketing surface. This size axis is the toolbar's own density knob: it deliberately does not inherit the page's comfortable/compact form density, because a control bar that shrinks to form-compact gets hard to hit. All keep a 40px hit area via an invisible vertical extender, so small controls never get harder to click than they look.
Groups, title, and spacing#
Build an app bar from three structural parts. ToolbarGroup wraps a run of controls into a named region (give it an aria-label and screen readers announce it), ToolbarTitle is a non-interactive label, and ToolbarSpacer is a flexible gap that pushes everything after it to the far end.
Toggle groups#
Wrap controls that hold their pressed state in a ToolbarToggleGroup. Use type="multiple" for independent on/off marks (bold, italic, underline) or type="single" for a mutually exclusive choice (text alignment). A selected item reads as a filled chip.
Dropdown triggers#
Set caret to make a control grow to fit its glyph and show a trailing chevron: the dropdown-trigger look. Compose it with our Dropdown Menu or Popover via asChild. Wrap the trigger in a Tooltip for a hint (that keeps the hint outside the trigger, so it never fights the menu's open state).
Vertical orientation#
Pass orientation="vertical" for a tool rail. Arrow-key navigation follows the axis, separators turn horizontal, and tooltipPlacement="right" keeps the hints clear of the column.
Beyond text#
The shell is generic. Here it holds media transport controls, showing the same buttons, toggle group, and separators serve a player just as well as an editor.
API reference#
Toolbar
The band. Wraps Radix Toolbar.Root and forwards its props (loop, dir, …).
variant—"floating"(default) ·"outline"·"plain"·"solid"(inverted, for overlays).size—"sm"·"md"(default) ·"lg". The toolbar's own density axis; it does not inherit the page'scomfortable/compactdensity.scrollable— scroll on the axis with an edge fade instead of wrapping when controls exceed the width.orientation—"horizontal"(default) ·"vertical". Drives both the layout and Radix's arrow-key axis.aria-label— names the toolbar for assistive tech. Defaults to"Toolbar"; set something specific.
ToolbarButton
An icon control (Radix Toolbar.Button).
tooltip— hover/focus hint; a string also becomes thearia-labelwhen none is set.shortcut— a keycap shown in the tooltip (display only, e.g."⌘B").pressed— mark the control active (setsaria-pressedand the filled chip).caret— trailing chevron + auto width for a dropdown trigger.asChild— render as a menu/popover trigger;static— drop the press scale.variant:"default"·"primary"(the bar's main action, brand-filled).
ToolbarToggleGroup · ToolbarToggleItem
Controls that hold a pressed state (Radix Toolbar.ToggleGroup). The group takes type ("single" / "multiple") plus value/onValueChange. Each item shares tooltip, shortcut, and tooltipPlacement.
ToolbarSeparator · ToolbarLink
A hairline that groups controls (orientation-aware), and a text link that lives in the bar and keeps the roving focus (Radix Toolbar.Separator / Toolbar.Link).
ToolbarGroup · ToolbarTitle · ToolbarSpacer
Structure parts. ToolbarGroup is an aria-labelled role="group" cluster; ToolbarTitle a non-interactive label; and ToolbarSpacer a flexible gap that pins later controls to the end.
ToolbarOverflow
A self-collapsing toolbar. Takes items (an array of action, toggle, and separator descriptors), renders as many as fit, and folds the rest into a three-dots menu, remeasuring on resize. Also takes overflowLabel, menuAlign, and the Toolbar variant/size props. Horizontal only.