Toolbar
FreeTools with one tab stop: arrows move between them, toggles sit recessed, and when the row runs out of width the last tools fold into a More menu with their state.
pnpm dlx shadcn@latest add https://caliperui.site/r/toolbar.jsonThe quarterly close finished two days early.
Tab into the toolbar, then use the arrow keys.
When to use #
Use it for
- Formatting bars, canvas tools and table actions.
- Rows of tools that must work at any width.
Skip it when
- Two or three related buttons; use a button group.
- Navigation between places; use tabs.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/toolbar.json
Adds its 7 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from Toolbar, ToolbarButton, ToolbarToggleGroup, ToolbarToggleItem, ToolbarSeparator and ToolbarItem.
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, ImagePlus, Italic, Link2, MessageSquarePlus, Redo2, Strikethrough, Underline, Undo2 } from "lucide-react" import { useState } from "react" import { SelectMenu } from "@/components/ui/select-menu" import { Slider } from "@/components/ui/slider" import { Toolbar, ToolbarButton, ToolbarItem, ToolbarSeparator, ToolbarToggleGroup, ToolbarToggleItem } from "@/components/ui/toolbar" const styles = [ { value: "p", label: "Paragraph" }, { value: "h2", label: "Heading" }, { value: "quote", label: "Quote" }, ]; export function ToolbarDemo() { const [marks, setMarks] = useState<string[]>(["bold"]); const [align, setAlign] = useState("left"); const [style, setStyle] = useState("p"); const [width, setWidth] = useState(100); const [last, setLast] = useState(""); return ( <div className="grid w-full max-w-2xl gap-4"> <div style={{ width: `${width}%` }} className="min-w-[180px]"> <Toolbar label="Formatting"> <ToolbarButton label="Undo" icon={<Undo2 aria-hidden="true" />} shortcut="mod+z" onClick={() => setLast("Undo")} /> <ToolbarButton label="Redo" icon={<Redo2 aria-hidden="true" />} shortcut="mod+shift+z" onClick={() => setLast("Redo")} /> <ToolbarSeparator /> <ToolbarItem> <SelectMenu aria-label="Text style" size="sm" variant="ghost" options={styles} value={style} onValueChange={setStyle} /> </ToolbarItem> <ToolbarSeparator /> <ToolbarToggleGroup label="Text marks" type="multiple" value={marks} onValueChange={setMarks}> <ToolbarToggleItem value="bold" label="Bold" icon={<Bold aria-hidden="true" />} shortcut="mod+b" /> <ToolbarToggleItem value="italic" label="Italic" icon={<Italic aria-hidden="true" />} shortcut="mod+i" /> <ToolbarToggleItem value="underline" label="Underline" icon={<Underline aria-hidden="true" />} shortcut="mod+u" /> <ToolbarToggleItem value="strike" label="Strikethrough" icon={<Strikethrough aria-hidden="true" />} /> </ToolbarToggleGroup> <ToolbarSeparator /> <ToolbarToggleGroup label="Alignment" type="single" value={align} onValueChange={setAlign}> <ToolbarToggleItem value="left" label="Align left" icon={<AlignLeft aria-hidden="true" />} /> <ToolbarToggleItem value="center" label="Align centre" icon={<AlignCenter aria-hidden="true" />} /> <ToolbarToggleItem value="right" label="Align right" icon={<AlignRight aria-hidden="true" />} /> <ToolbarToggleItem value="justify" label="Justify" icon={<AlignJustify aria-hidden="true" />} /> </ToolbarToggleGroup> <ToolbarSeparator /> <ToolbarButton label="Insert link" icon={<Link2 aria-hidden="true" />} shortcut="mod+k" onClick={() => setLast("Insert link")} /> <ToolbarButton label="Insert image" icon={<ImagePlus aria-hidden="true" />} onClick={() => setLast("Insert image")} /> <ToolbarButton label="Comment" icon={<MessageSquarePlus aria-hidden="true" />} onClick={() => setLast("Comment")} /> </Toolbar> </div> <p className="max-w-[60ch] rounded-[12px] border border-border p-4 leading-relaxed" style={{ fontWeight: marks.includes("bold") ? 600 : 400, fontStyle: marks.includes("italic") ? "italic" : "normal", textDecoration: [marks.includes("underline") && "underline", marks.includes("strike") && "line-through"].filter(Boolean).join(" ") || "none", textAlign: align as "left", fontSize: style === "h2" ? 20 : 14, borderInlineStart: style === "quote" ? "3px solid var(--border-strong)" : undefined, }} > The quarterly close finished two days early. </p> <div className="grid max-w-xs gap-1.5"> <label htmlFor="toolbar-width" className="text-[13px] text-muted-foreground">Toolbar width: {width}%</label> <Slider id="toolbar-width" min={30} max={100} value={width} onChange={(event) => setWidth(Number(event.target.value))} /> </div> <p role="status" className="text-[13px] text-muted-foreground">{last ? `Last action: ${last}` : "Tab into the toolbar, then use the arrow keys."}</p> </div> ); }
API reference #
Toolbar
component
Also accepts the native props of its root element and a ref.
ToolbarButton
component
ToolbarToggleGroup
component
ToolbarToggleItem
component
ToolbarSeparator
component
No props of its own; it re-exports a primitive and takes that primitive’s props.
ToolbarItem
component
Accessibility #
It exposes toolbar and separator semantics, with aria-label, aria-orientation, aria-hidden, aria-pressed and aria-checked kept in step with state.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.