caliper uidocs

Toolbar

Free

Tools 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.

Installs components/ui/toolbar.tsxDepends on @radix-ui/react-dropdown-menu, @radix-ui/react-slot, @radix-ui/react-tooltip, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/toolbar.json

The 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 #

terminal
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.

example.tsx
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.

PropTypeDefaultDescription
label *string—Names the toolbar, e.g. "Formatting".
orientation"horizontal" | "vertical""horizontal"
overflowbooleantrueFold tools that do not fit into a More menu (horizontal only).

ToolbarButton

component
PropTypeDefaultDescription
label *string—
iconReactNode—
showLabelboolean!icon
shortcutstring—Shown in the tooltip, e.g. "mod+b". Your app listens for the keys.
pressedboolean—
onPressedChange((pressed: boolean) => void)—
onClick(() => void)—
pinnedbooleanfalseNever folds into the More menu.
disabledboolean—
classNamestring—

ToolbarToggleGroup

component
PropTypeDefault
label *string—
pinnedbooleanfalse
children *ReactNode—
type *"multiple" | "single"—
value *string | readonly string[]—
onValueChange *((value: string) => void) | ((value: string[]) => void)—

ToolbarToggleItem

component
PropTypeDefault
value *string—
label *string—
iconReactNode—
shortcutstring—
disabledboolean—

ToolbarSeparator

component

No props of its own; it re-exports a primitive and takes that primitive’s props.

ToolbarItem

component
PropTypeDefault
children *ReactNode—
classNamestring—

Accessibility #

It exposes toolbar and separator semantics, with aria-label, aria-orientation, aria-hidden, aria-pressed and aria-checked kept in step with state.

KeyBehaviour
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.

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.