# Toolbar

A bar of buttons, toggles and links that the keyboard reaches as one stop and moves through with the arrows.

- **Import:** `import { Toolbar, ToolbarGroup, ToolbarButton, ToolbarOverflowButton, ToolbarLink, ToolbarToggleGroup, ToolbarToggleItem, ToolbarSeparator } from "@booleanpress/ui/toolbar"`
- **Radix Toolbar:** <https://www.radix-ui.com/primitives/docs/components/toolbar>
- **APG Toolbar:** <https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/>
- **Page:** <https://ui.booleanpress.com/components/toolbar> · @booleanpress/ui 0.2.0

## Usage

A toolbar groups the controls that act on one thing: an editor's formatting, a list's bulk actions. Tab reaches it once; the arrow keys move between its buttons. Use it for three or more controls; two buttons side by side are a [button group](/components/button-group).

```tsx
import { BoldIcon, ItalicIcon } from "lucide-react"
import { Toolbar, ToolbarButton } from "@booleanpress/ui/toolbar"

export function Formatting() {
  return (
    <Toolbar aria-label="Formatting">
      <ToolbarButton aria-label="Bold"><BoldIcon /></ToolbarButton>
      <ToolbarButton aria-label="Italic"><ItalicIcon /></ToolbarButton>
    </Toolbar>
  )
}
```

Name the toolbar with `aria-label`. `ToolbarGroup` gathers controls: the bar spreads its groups out, so two groups sit at the start and the end, and three at the start, centre and end. `ToolbarButton` takes Button's `variant`, `size`, `severity` and `loading`, and defaults to the muted ghost look. `ToolbarToggleGroup` and `ToolbarToggleItem` are a toggle group inside the toolbar (`type="single"` or `"multiple"`), `ToolbarLink` a link, and `ToolbarSeparator` a line between groups. `ToolbarOverflowButton` is an ellipsis button named by the provider's `moreActions` string: put it in a [dropdown menu](/components/dropdown-menu)'s trigger for the actions that do not fit. `orientation="vertical"` stacks the controls and moves with ↑ and ↓. A field inside the bar, such as a search box, is its own tab stop.

## Examples

### Basic

An editor's formatting buttons in three groups, with a disabled Redo the arrows skip.

```tsx
import { BoldIcon, ItalicIcon, LinkIcon, ListIcon, ListOrderedIcon, Redo2Icon, Undo2Icon } from "lucide-react"
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator } from "@booleanpress/ui/toolbar"

export default function ToolbarBasic() {
  return (
    <Toolbar aria-label="Email template formatting" className="w-full max-w-xl justify-start">
      <ToolbarGroup>
        <ToolbarButton aria-label="Undo">
          <Undo2Icon />
        </ToolbarButton>
        <ToolbarButton aria-label="Redo" disabled>
          <Redo2Icon />
        </ToolbarButton>
      </ToolbarGroup>
      <ToolbarSeparator />
      <ToolbarGroup>
        <ToolbarButton aria-label="Bold">
          <BoldIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Italic">
          <ItalicIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Insert link">
          <LinkIcon />
        </ToolbarButton>
      </ToolbarGroup>
      <ToolbarSeparator />
      <ToolbarGroup>
        <ToolbarButton aria-label="Bulleted list">
          <ListIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Numbered list">
          <ListOrderedIcon />
        </ToolbarButton>
      </ToolbarGroup>
    </Toolbar>
  )
}
```

### Start, centre and end

Three groups spread across the bar: icon buttons, a search field and the form's buttons.

```tsx
import { PlusIcon, PrinterIcon, UploadIcon } from "lucide-react"
import { Input } from "@booleanpress/ui/input"
import { Toolbar, ToolbarButton, ToolbarGroup } from "@booleanpress/ui/toolbar"

export default function ToolbarStartCenterEnd() {
  return (
    <Toolbar aria-label="Contacts" className="w-full max-w-2xl">
      <ToolbarGroup>
        <ToolbarButton aria-label="New contact">
          <PlusIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Print">
          <PrinterIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Import contacts">
          <UploadIcon />
        </ToolbarButton>
      </ToolbarGroup>
      <ToolbarGroup>
        <Input aria-label="Search contacts" placeholder="Search" className="w-48" />
      </ToolbarGroup>
      <ToolbarGroup>
        <ToolbarButton variant="outline" size="sm">
          Cancel
        </ToolbarButton>
        <ToolbarButton variant="default" size="sm">
          Save
        </ToolbarButton>
      </ToolbarGroup>
    </Toolbar>
  )
}
```

### With toggle groups

A `multiple` group for the text style and a `single` group for the alignment.

```tsx
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react"
import {
  Toolbar,
  ToolbarButton,
  ToolbarSeparator,
  ToolbarToggleGroup,
  ToolbarToggleItem,
} from "@booleanpress/ui/toolbar"

export default function ToolbarToggleGroups() {
  return (
    <Toolbar aria-label="Signature formatting" className="w-full max-w-xl justify-start">
      <ToolbarToggleGroup type="multiple" aria-label="Text style" defaultValue={["bold"]}>
        <ToolbarToggleItem value="bold" aria-label="Bold">
          <BoldIcon />
        </ToolbarToggleItem>
        <ToolbarToggleItem value="italic" aria-label="Italic">
          <ItalicIcon />
        </ToolbarToggleItem>
        <ToolbarToggleItem value="underline" aria-label="Underline">
          <UnderlineIcon />
        </ToolbarToggleItem>
      </ToolbarToggleGroup>
      <ToolbarSeparator />
      <ToolbarToggleGroup type="single" aria-label="Alignment" defaultValue="left">
        <ToolbarToggleItem value="left" aria-label="Align left">
          <AlignLeftIcon />
        </ToolbarToggleItem>
        <ToolbarToggleItem value="center" aria-label="Align centre">
          <AlignCenterIcon />
        </ToolbarToggleItem>
        <ToolbarToggleItem value="right" aria-label="Align right">
          <AlignRightIcon />
        </ToolbarToggleItem>
      </ToolbarToggleGroup>
      <ToolbarButton variant="outline" size="sm" className="ms-auto">
        Preview
      </ToolbarButton>
    </Toolbar>
  )
}
```

### Vertical

`orientation="vertical"` stacks the buttons; ↑ and ↓ move between them.

```tsx
import { DownloadIcon, FilterIcon, RefreshCwIcon, SettingsIcon } from "lucide-react"
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator } from "@booleanpress/ui/toolbar"

export default function ToolbarVertical() {
  return (
    <Toolbar orientation="vertical" aria-label="Delivery log">
      <ToolbarGroup>
        <ToolbarButton aria-label="Refresh">
          <RefreshCwIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Filter">
          <FilterIcon />
        </ToolbarButton>
        <ToolbarButton aria-label="Export as CSV">
          <DownloadIcon />
        </ToolbarButton>
      </ToolbarGroup>
      <ToolbarSeparator />
      <ToolbarButton aria-label="Log settings">
        <SettingsIcon />
      </ToolbarButton>
    </Toolbar>
  )
}
```

### Overflow menu

A `ToolbarOverflowButton` at the end opens a dropdown menu of further actions.

```tsx
import { ArchiveIcon, CopyIcon, MailIcon, ReplyIcon, Trash2Icon } from "lucide-react"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarOverflowButton } from "@booleanpress/ui/toolbar"

export default function ToolbarOverflowMenu() {
  return (
    <Toolbar aria-label="Ticket #4821" className="w-full max-w-md">
      <ToolbarGroup>
        <ToolbarButton>
          <ReplyIcon /> Reply
        </ToolbarButton>
        <ToolbarButton>
          <ArchiveIcon /> Close ticket
        </ToolbarButton>
      </ToolbarGroup>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          {/* Named by the provider's "More actions" string. */}
          <ToolbarOverflowButton />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem>
            <MailIcon /> Mark as unread
          </DropdownMenuItem>
          <DropdownMenuItem>
            <CopyIcon /> Copy link
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem variant="destructive">
            <Trash2Icon /> Delete ticket
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </Toolbar>
  )
}
```

## Accessibility

**Semantics.** A `div` with `role="toolbar"` and `aria-orientation`. Buttons and links are native; toggle items are buttons with `aria-pressed` (`role="radio"` and `aria-checked` in a `single` group); a group is `role="group"`; a separator is `role="separator"`.

**Labels.** Name the toolbar (`aria-label` or `aria-labelledby`) and every icon-only control. The overflow button is named by the provider's `moreActions` string unless you pass `aria-label`.

**Focus.** The toolbar is one tab stop: Tab lands on the last control used (the first at the start), the arrows move from control to control, wrapping at the ends, and Tab leaves. Disabled controls are skipped. Focus is a 1px `--ring` outline 2px outside the control.

**Known limits.**

- A text field inside the toolbar keeps its own arrow keys and is a separate tab stop.
- Groups wrap onto a new line when the bar is narrow; the arrow keys follow the source order.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves into the toolbar, onto the last control used, and out again: one stop. |
| → or ← | Moves to the next or previous control, wrapping at the ends (↓ and ↑ when vertical). Reversed in a right-to-left page. |
| Home or End | Moves to the first or last control. |
| Enter or Space | Activates the focused button, or toggles the focused toggle item. |

## API

### Toolbar

Renders Radix Toolbar.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `dir` | `"ltr" \| "rtl"` |  | The reading direction. The provider's `dir` by default. |
| `loop` | `boolean` |  | Whether the arrows wrap from the last control to the first. `true` by default. |
| `orientation` | `"horizontal" \| "vertical"` | `horizontal` | `horizontal` (default) or `vertical`: the layout, and which arrow keys move. |

### ToolbarGroup

Renders a `div` and passes it every other prop.

### ToolbarButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data-slot` | `string` | `toolbar-button` |  |
| `loading` | `boolean` |  | Shows the spinner and sets `aria-busy` and `aria-disabled`, as Button's: the button ignores presses but keeps its focus, and the arrow keys still reach it. |
| `raised` | `boolean \| null` |  | Lifts the button on a shadow. |
| `rounded` | `boolean \| null` |  | A pill, or a circle for an icon button. |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  | Button's severity colour: `success`, `info`, `warning`, `help`, `danger`, `contrast`. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` |  | Button's size: `default`, `xs`, `sm`, `lg`, or a square `icon` size. |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | `ghost` | The button's look, as Button's. `ghost` by default: muted text that takes the faintest surface on hover. |

### ToolbarOverflowButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` |  | Shows the spinner and sets `aria-busy` and `aria-disabled`, as Button's: the button ignores presses but keeps its focus, and the arrow keys still reach it. |
| `raised` | `boolean \| null` |  |  |
| `rounded` | `boolean \| null` |  |  |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  |  |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` |  |  |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` |  | The button's look, as Button's. `ghost` by default: muted text that takes the faintest surface on hover. |

### ToolbarLink

Renders Radix Toolbar.Link and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element (a router's link) instead, with the link's behaviour and classes merged onto it. |

### ToolbarToggleGroup

Renders Radix Toolbar.ToggleGroup and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type` (required) | `"single" \| "multiple"` |  | `single` (one item on at a time) or `multiple`. Required. |
| `asChild` | `boolean` |  |  |
| `defaultValue` | `string \| string[]` |  | The value of the item that is pressed when initially rendered. Use `defaultValue` if you do not need to control the state of a toggle group. The value of the items that are pressed when initially rendered. Use `defaultValue` if you do not need to control the state of a toggle group. |
| `dir` | `"ltr" \| "rtl"` |  |  |
| `disabled` | `boolean` |  | Whether the group is disabled from user interaction. |
| `loop` | `boolean` |  |  |
| `onValueChange` | `((value: string) => void) \| ((value: string[]) => void)` |  | The callback that fires when the value of the toggle group changes. The callback that fires when the state of the toggle group changes. |
| `orientation` | `"horizontal" \| "vertical"` |  |  |
| `rovingFocus` | `boolean` |  | Whether the group should maintain roving focus of its buttons. |
| `size` | `"default" \| "sm" \| "lg" \| null` |  | `sm`, `default` or `lg`, for every item. Defaults to the provider's `controlSize`. |
| `value` | `string \| string[]` |  | The controlled stateful value of the item that is pressed. The controlled stateful value of the items that are pressed. |

### ToolbarToggleItem

Renders Radix Toolbar.ToggleItem and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `value` | `string \| string[]` |  | The controlled stateful value of the item that is pressed. The controlled stateful value of the items that are pressed. |

### ToolbarSeparator

Renders Radix Toolbar.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `decorative` | `boolean` |  | Whether or not the component is purely decorative. When true, accessibility-related attributes are updated so that that the rendered element is removed from the accessibility tree. |
| `orientation` | `"horizontal" \| "vertical"` |  | Either `vertical` or `horizontal`. Defaults to `horizontal`. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="toolbar"` (Toolbar), `data-slot="toolbar-group"` (ToolbarGroup), `data-slot="toolbar-overflow-button"` (ToolbarOverflowButton), `data-slot="toolbar-link"` (ToolbarLink), `data-slot="toolbar-toggle-group"` (ToolbarToggleGroup), `data-slot="toolbar-toggle-item"` (ToolbarToggleItem), `data-slot="toolbar-separator"` (ToolbarSeparator), and `data-size`.

**Provider strings:** `moreActions` (`BooleanUIProvider`'s `strings`).

## Theming

The bar is `--card` with a 1px `--border` edge, a 6px radius and 10px padding. Buttons are `--muted-foreground` on transparent, with `--subtle` under the pointer; toggle items take the toggle's look; separators are `--border`; focus is `--ring`.

| Token | Used for |
| --- | --- |
| `--accent-foreground` | text |
| `--border` | background |
| `--card` | background |
| `--card-foreground` | text |
| `--muted-foreground` | text |
