# Toolbar

Groups related commands and controls with arrow-key focus navigation.

Page: https://sui.draco.dev/docs/components/toolbar

Toolbar gives a set of actions a shared keyboard navigation model. Compose buttons, toggle groups, links, and a final input while keeping each action's own accessible name.

### Example: toolbar-demo

```tsx
import { Toggle } from "@workspace/ui/components/toggle";
import { ToggleGroup } from "@workspace/ui/components/toggle-group";
import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarLink,
  ToolbarSeparator,
} from "@workspace/ui/components/toolbar";
import { cn } from "cn";
import {
  BoldIcon,
  ItalicIcon,
  RotateCcwIcon,
  UnderlineIcon,
} from "lucide-react";
import { useState } from "react";
import type { ExampleProps } from "../types";

export default function ToolbarDemo({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const [formats, setFormats] = useState<string[]>([]);

  return (
    <div className="flex max-w-lg flex-col items-start gap-4">
      <Toolbar
        aria-label={chinese ? "文字格式工具栏" : "Text formatting toolbar"}
      >
        <ToolbarGroup aria-label={chinese ? "文字格式" : "Text formatting"}>
          <ToggleGroup
            multiple
            value={formats}
            onValueChange={setFormats}
            spacing={1}
          >
            <ToolbarButton
              render={<Toggle />}
              value="bold"
              size="icon"
              aria-label={chinese ? "加粗" : "Bold"}
            >
              <BoldIcon />
            </ToolbarButton>
            <ToolbarButton
              render={<Toggle />}
              value="italic"
              size="icon"
              aria-label={chinese ? "斜体" : "Italic"}
            >
              <ItalicIcon />
            </ToolbarButton>
            <ToolbarButton
              render={<Toggle />}
              value="underline"
              size="icon"
              aria-label={chinese ? "下划线" : "Underline"}
            >
              <UnderlineIcon />
            </ToolbarButton>
          </ToggleGroup>
        </ToolbarGroup>
        <ToolbarSeparator />
        <ToolbarButton
          size="icon"
          aria-label={chinese ? "清除格式" : "Reset formatting"}
          onClick={() => setFormats([])}
        >
          <RotateCcwIcon />
        </ToolbarButton>
        <ToolbarLink href={chinese ? "/zh-CN/docs" : "/docs"}>
          {chinese ? "帮助" : "Help"}
        </ToolbarLink>
      </Toolbar>
      <p
        className={cn(
          "text-sm",
          formats.includes("bold") && "font-medium",
          formats.includes("italic") && "italic",
          formats.includes("underline") && "underline",
        )}
      >
        {chinese
          ? "选中文字格式，预览会即时更新"
          : "Choose a text format to update this preview."}
      </p>
    </div>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/toolbar
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

## Usage

```tsx showLineNumbers
import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSeparator,
} from "@workspace/ui/components/toolbar"

<Toolbar aria-label="Document actions">
  <ToolbarGroup aria-label="Editing actions">
    <ToolbarButton onClick={undo}>Undo</ToolbarButton>
    <ToolbarButton onClick={redo} disabled={!canRedo}>Redo</ToolbarButton>
  </ToolbarGroup>
  <ToolbarSeparator />
  <ToolbarButton onClick={save}>Save</ToolbarButton>
</Toolbar>
```

## Toggle and trigger composition

`ToolbarButton` supports the same `variant` and `size` values as `Button`, defaulting to `ghost` and `default`. Use `render` to compose it with `Toggle`, a menu trigger, or another Base UI control. `ToggleGroup` manages related format choices, while the toolbar coordinates focus among controls.

Use `ToolbarGroup` for related actions and `ToolbarSeparator` between groups. A separator automatically uses the orientation opposite to its toolbar.

## Search input

`ToolbarInput` participates in the toolbar's keyboard model. In a horizontal toolbar, place a single input last so text editing and horizontal focus movement stay predictable. The example filters real notes and moves between the matching results.

### Example: toolbar-search

```tsx
import {
  Toolbar,
  ToolbarButton,
  ToolbarInput,
  ToolbarSeparator,
} from "@workspace/ui/components/toolbar";
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
import { useState } from "react";
import type { ExampleProps } from "../types";

export default function ToolbarSearch({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const noMatchesLabel = chinese ? "没有匹配的笔记" : "No matching notes.";
  const [query, setQuery] = useState("");
  const [position, setPosition] = useState(0);
  const notes = chinese
    ? ["检查键盘交互", "调整组件主题", "更新双语文档", "发布组件示例"]
    : [
        "Check keyboard interactions",
        "Customize component themes",
        "Update bilingual documentation",
        "Publish component examples",
      ];
  const filtered = notes.filter((note) =>
    note.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
  );
  const selected = filtered.length ? position % filtered.length : 0;

  return (
    <div className="flex w-full max-w-md flex-col items-start gap-4">
      <Toolbar aria-label={chinese ? "查找笔记" : "Find notes"}>
        <ToolbarButton
          size="icon"
          aria-label={chinese ? "上一条结果" : "Previous result"}
          disabled={!filtered.length}
          onClick={() =>
            setPosition(
              (current) => (current + filtered.length - 1) % filtered.length,
            )
          }
        >
          <ArrowUpIcon />
        </ToolbarButton>
        <ToolbarButton
          size="icon"
          aria-label={chinese ? "下一条结果" : "Next result"}
          disabled={!filtered.length}
          onClick={() =>
            setPosition((current) => (current + 1) % filtered.length)
          }
        >
          <ArrowDownIcon />
        </ToolbarButton>
        <ToolbarSeparator />
        <ToolbarInput
          aria-label={chinese ? "搜索笔记" : "Search notes"}
          placeholder={chinese ? "搜索笔记…" : "Search notes…"}
          value={query}
          onChange={(event) => {
            setQuery(event.target.value);
            setPosition(0);
          }}
        />
      </Toolbar>
      <output className="text-muted-foreground text-sm" aria-live="polite">
        {filtered.length
          ? `${selected + 1} / ${filtered.length}: ${filtered[selected]}`
          : noMatchesLabel}
      </output>
    </div>
  );
}
```

## Vertical and disabled toolbars

Set `orientation="vertical"` to use a vertical layout and Up/Down focus navigation. `disabled` on `Toolbar` disables button and input controls; `disabled` on `ToolbarGroup` disables a set of actions. Buttons can also be disabled independently.

### Example: toolbar-disabled

```tsx
import { Field, FieldLabel } from "@workspace/ui/components/field";
import { Switch } from "@workspace/ui/components/switch";
import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSeparator,
} from "@workspace/ui/components/toolbar";
import { RotateCcwIcon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
import { useId, useState } from "react";
import type { ExampleProps } from "../types";

export default function ToolbarDisabled({ locale }: ExampleProps) {
  const chinese = locale === "zh-CN";
  const id = useId();
  const [disabled, setDisabled] = useState(true);
  const [zoom, setZoom] = useState(100);

  return (
    <div className="flex flex-col items-start gap-4">
      <Field orientation="horizontal">
        <Switch id={id} checked={disabled} onCheckedChange={setDisabled} />
        <FieldLabel htmlFor={id}>
          {chinese ? "禁用工具栏" : "Disable toolbar"}
        </FieldLabel>
      </Field>
      <div className="flex items-center gap-4">
        <Toolbar
          orientation="vertical"
          disabled={disabled}
          aria-label={chinese ? "缩放工具栏" : "Zoom toolbar"}
        >
          <ToolbarGroup aria-label={chinese ? "调整缩放" : "Adjust zoom"}>
            <ToolbarButton
              size="icon"
              aria-label={chinese ? "放大" : "Zoom in"}
              onClick={() => setZoom((value) => Math.min(200, value + 10))}
              disabled={zoom >= 200}
            >
              <ZoomInIcon />
            </ToolbarButton>
            <ToolbarButton
              size="icon"
              aria-label={chinese ? "缩小" : "Zoom out"}
              onClick={() => setZoom((value) => Math.max(50, value - 10))}
              disabled={zoom <= 50}
            >
              <ZoomOutIcon />
            </ToolbarButton>
          </ToolbarGroup>
          <ToolbarSeparator />
          <ToolbarButton
            size="icon"
            aria-label={chinese ? "重置缩放" : "Reset zoom"}
            onClick={() => setZoom(100)}
          >
            <RotateCcwIcon />
          </ToolbarButton>
        </Toolbar>
        <output className="text-muted-foreground text-sm" aria-live="polite">
          {chinese ? "缩放" : "Zoom"}: {zoom}%
        </output>
      </div>
    </div>
  );
}
```

## Accessibility and keyboard

Give the toolbar an `aria-label` describing its purpose. Label each icon button, and label groups when their purpose adds useful context. Tab enters the toolbar, arrow keys move between its controls, and Tab leaves it. Focus wraps by default; set `loopFocus={false}` to stop at the ends.

Base UI disabled buttons remain focusable by default. Use `focusableWhenDisabled={false}` when disabled controls should be skipped. Keep native event handlers and the `render` composition intact so keyboard activation follows the same path as pointer interaction.

## API reference

| Part | Purpose |
| --- | --- |
| `Toolbar` | Accepts `orientation` (`horizontal` by default), `disabled`, `loopFocus`, normal root props, and `ref`. |
| `ToolbarButton` | A toolbar action with `variant`, `size`, `disabled`, `focusableWhenDisabled`, and `render`. |
| `ToolbarLink` | A navigable link with Button `variant`/`size`, anchor props, and `render`. |
| `ToolbarInput` | A text input integrated with toolbar focus navigation, styled through `Input`. |
| `ToolbarGroup` | Groups related controls and accepts a group-level `disabled` state. |
| `ToolbarSeparator` | Separates groups; its direction follows the toolbar unless explicitly overridden. |

See the [Base UI Toolbar API](https://base-ui.com/react/components/toolbar#api-reference) for all primitive props and keyboard behavior.
