# Toolbar

Give buttons, toggles, menus and fields one tab stop with arrow-key navigation.

Use a Toolbar to group the controls of an editor, a canvas or a table. Tab enters it once, and the arrow keys move between its buttons, toggles, menus and fields.

```svelte title="demo.svelte"
<script lang="ts">
  import { BoldIcon, ItalicIcon, LinkIcon, UnderlineIcon } from "@lucide/svelte"
  import { Toggle, ToggleIcon } from "@ui-sdk/svelte/toggle"
  import {
    Toolbar,
    ToolbarButton,
    ToolbarSeparator,
  } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const styles = [
    { label: "Bold", icon: BoldIcon },
    { label: "Italic", icon: ItalicIcon },
    { label: "Underline", icon: UnderlineIcon },
  ]

  const log = useLog()
</script>

<Toolbar aria-label="Formatting">
  {#each styles as { label, icon: Icon } (label)}
    <Toggle
      aria-label={label}
      onPressedChange={(pressed) => log(`${label} ${pressed ? "on" : "off"}`)}
    >
      <ToggleIcon>
        <Icon />
      </ToggleIcon>
    </Toggle>
  {/each}
  <ToolbarSeparator />
  <ToolbarButton aria-label="Insert link" onclick={() => log("Insert link")}>
    <LinkIcon aria-hidden="true" />
  </ToolbarButton>
</Toolbar>
```

Tab into the toolbar, then use Left and Right to move between Bold, Italic, Underline and Insert link. Press Space to toggle the focused style. The console logs each change.

## Installation

```bash
npm install @ui-sdk/svelte @ui-sdk/core
```

## Composition

Use the following composition to build a `Toolbar`:

```text
Toolbar
├── ToolbarButton
├── ToolbarSeparator
├── ToolbarGroup
│   ├── ToolbarButton
│   └── ToolbarButton
├── ToolbarLink
└── ToolbarInput
```

Put buttons, links and fields straight inside the toolbar. `ToolbarGroup` names a set of related items, and `ToolbarSeparator` draws a line between sets. Toggles and toggle groups join the toolbar on their own, and menu and select triggers join through `ToolbarButton`.

## Usage

Import the parts and put them together:

```svelte
<script lang="ts">
  import {
    Toolbar,
    ToolbarButton,
    ToolbarSeparator,
  } from "@ui-sdk/svelte/toolbar"
</script>

<Toolbar aria-label="Formatting">
  <ToolbarButton>Undo</ToolbarButton>
  <ToolbarButton>Redo</ToolbarButton>
  <ToolbarSeparator />
  <ToolbarButton>Insert Link</ToolbarButton>
</Toolbar>
```

Name the toolbar with `aria-label`, and give every icon-only control its own.

## Toggle Groups

A [Toggle](/docs/svelte/components/toggle) group inside a toolbar joins its navigation instead of adding another tab stop. Arrow keys move focus without pressing anything; Enter or Space presses the focused toggle.

```svelte title="toggle-group.svelte"
<script lang="ts">
  import {
    AlignCenterIcon,
    AlignLeftIcon,
    AlignRightIcon,
    ListIcon,
    ListOrderedIcon,
  } from "@lucide/svelte"
  import { Toggle, ToggleGroup, ToggleIcon } from "@ui-sdk/svelte/toggle"
  import { Toolbar, ToolbarSeparator } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const alignments = [
    { value: "left", label: "Align left", icon: AlignLeftIcon },
    { value: "center", label: "Align center", icon: AlignCenterIcon },
    { value: "right", label: "Align right", icon: AlignRightIcon },
  ]

  const lists = [
    { value: "bulleted", label: "Bulleted list", icon: ListIcon },
    { value: "numbered", label: "Numbered list", icon: ListOrderedIcon },
  ]

  const log = useLog()
</script>

<Toolbar aria-label="Paragraph">
  <ToggleGroup
    defaultValue={["left"]}
    deselectable={false}
    aria-label="Alignment"
    onValueChange={([value]) => log(`Align ${value}`)}
  >
    {#each alignments as { value, label, icon: Icon } (value)}
      <Toggle {value} aria-label={label}>
        <ToggleIcon>
          <Icon />
        </ToggleIcon>
      </Toggle>
    {/each}
  </ToggleGroup>
  <ToolbarSeparator />
  <ToggleGroup
    aria-label="List"
    onValueChange={([value]) =>
      log(lists.find((list) => list.value === value)?.label ?? "No list")}
  >
    {#each lists as { value, label, icon: Icon } (value)}
      <Toggle {value} aria-label={label}>
        <ToggleIcon>
          <Icon />
        </ToggleIcon>
      </Toggle>
    {/each}
  </ToggleGroup>
</Toolbar>
```

Arrow from the alignment group to the list group, then press Space on Align center or Numbered list. The console logs each choice.

## Groups

Use `ToolbarGroup` to name related controls. Set `disabled` on the group to disable them together; they stay focusable with `aria-disabled`.

```svelte title="group.svelte"
<script lang="ts">
  import {
    ClipboardIcon,
    CopyIcon,
    RedoIcon,
    ScissorsIcon,
    UndoIcon,
  } from "@lucide/svelte"
  import {
    Toolbar,
    ToolbarButton,
    ToolbarGroup,
    ToolbarSeparator,
  } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const clipboard = [
    { label: "Cut", icon: ScissorsIcon },
    { label: "Copy", icon: CopyIcon },
    { label: "Paste", icon: ClipboardIcon },
  ]

  const log = useLog()
</script>

<Toolbar aria-label="Edit">
  <ToolbarGroup aria-label="Clipboard">
    {#each clipboard as { label, icon: Icon } (label)}
      <ToolbarButton aria-label={label} onclick={() => log(label)}>
        <Icon aria-hidden="true" />
      </ToolbarButton>
    {/each}
  </ToolbarGroup>
  <ToolbarSeparator />
  <ToolbarGroup disabled aria-label="History">
    <ToolbarButton aria-label="Undo">
      <UndoIcon aria-hidden="true" />
    </ToolbarButton>
    <ToolbarButton aria-label="Redo">
      <RedoIcon aria-hidden="true" />
    </ToolbarButton>
  </ToolbarGroup>
</Toolbar>
```

Move through Cut, Copy and Paste into History with the arrow keys. Undo and Redo stay focusable but do nothing.

## Menus

Render a [Menu](/docs/svelte/components/menu) trigger through `ToolbarButton`. In a horizontal toolbar, Down opens the menu while Left and Right keep moving between toolbar items.

```svelte
<Menu>
  <ToolbarButton>
    {#snippet render({ props })}
      <MenuTrigger {...props}>Share</MenuTrigger>
    {/snippet}
  </ToolbarButton>
  <MenuContent>…</MenuContent>
</Menu>
```

```svelte title="menu.svelte"
<script lang="ts">
  import { ChevronDownIcon } from "@lucide/svelte"
  import { Menu, MenuContent, MenuItem, MenuTrigger } from "@ui-sdk/svelte/menu"
  import { Toolbar, ToolbarButton } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const log = useLog()
</script>

<Toolbar aria-label="Document">
  <ToolbarButton onclick={() => log("Saved")}>Save</ToolbarButton>
  <Menu onAction={(value) => log(`Shared by ${value}`)}>
    <ToolbarButton>
      {#snippet render({ props })}
        <MenuTrigger {...props}>
          Share
          <ChevronDownIcon aria-hidden="true" />
        </MenuTrigger>
      {/snippet}
    </ToolbarButton>
    <MenuContent>
      <MenuItem value="email">Email</MenuItem>
      <MenuItem value="link">Copy Link</MenuItem>
    </MenuContent>
  </Menu>
  <ToolbarButton onclick={() => log("Printed")}>Print</ToolbarButton>
</Toolbar>
```

Focus Share and press Down, then choose Email or Copy Link. Escape closes the menu and returns focus to Share.

## Selects

A [Select](/docs/svelte/components/select) trigger joins the same way. It keeps its select behavior while it takes part in the toolbar's focus order.

```svelte title="select.svelte"
<script lang="ts">
  import {
    CheckIcon,
    ChevronsUpDownIcon,
    ZoomInIcon,
    ZoomOutIcon,
  } from "@lucide/svelte"
  import {
    Select,
    SelectContent,
    SelectItem,
    SelectItemIndicator,
    SelectTrigger,
    SelectValue,
  } from "@ui-sdk/svelte/select"
  import { Toolbar, ToolbarButton } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const zooms = ["50%", "75%", "100%", "150%", "200%"]

  const log = useLog()
  let zoom = $state("100%")
  const index = $derived(zooms.indexOf(zoom))

  function change(value: string) {
    zoom = value
    log(`Zoom ${value}`)
  }
</script>

<Toolbar aria-label="View">
  <ToolbarButton
    aria-label="Zoom out"
    disabled={index === 0}
    onclick={() => change(zooms[index - 1])}
  >
    <ZoomOutIcon aria-hidden="true" />
  </ToolbarButton>
  <Select value={zoom} onValueChange={(value) => value && change(value)}>
    <ToolbarButton aria-label="Zoom">
      {#snippet render({ props })}
        <SelectTrigger {...props}>
          <SelectValue />
          <ChevronsUpDownIcon aria-hidden="true" />
        </SelectTrigger>
      {/snippet}
    </ToolbarButton>
    <SelectContent>
      {#each zooms as zoom (zoom)}
        <SelectItem value={zoom}>
          {zoom}
          <SelectItemIndicator>
            <CheckIcon aria-hidden="true" />
          </SelectItemIndicator>
        </SelectItem>
      {/each}
    </SelectContent>
  </Select>
  <ToolbarButton
    aria-label="Zoom in"
    disabled={index === zooms.length - 1}
    onclick={() => change(zooms[index + 1])}
  >
    <ZoomInIcon aria-hidden="true" />
  </ToolbarButton>
</Toolbar>
```

Open Zoom and choose a value, or step it with Zoom out and Zoom in. Each button turns off at the end of the range.

## Fields

Use `ToolbarInput` for a text field, or render another field through it, like a [Number Input](/docs/svelte/components/number-input) control. A field keeps Home, End and the arrow keys while they move its caret. At the matching end, with no text selected, an arrow moves to the next item.

Entering a field with an arrow puts its caret at the far end in that direction: press the same arrow again to leave, or the other arrow to edit. A number input keeps Up and Down for stepping its value.

```svelte title="input.svelte"
<script lang="ts">
  import { MinusIcon, PlusIcon } from "@lucide/svelte"
  import {
    NumberInput,
    NumberInputControl,
    NumberInputDecrement,
    NumberInputGroup,
    NumberInputIncrement,
  } from "@ui-sdk/svelte/number-input"
  import {
    Toolbar,
    ToolbarInput,
    ToolbarSeparator,
  } from "@ui-sdk/svelte/toolbar"
</script>

<Toolbar aria-label="Text">
  <NumberInput defaultValue={14} min={8} max={72}>
    <NumberInputGroup>
      <NumberInputDecrement aria-label="Decrease font size">
        <MinusIcon aria-hidden="true" />
      </NumberInputDecrement>
      <ToolbarInput aria-label="Font size">
        {#snippet render({ props })}
          <NumberInputControl {...props} />
        {/snippet}
      </ToolbarInput>
      <NumberInputIncrement aria-label="Increase font size">
        <PlusIcon aria-hidden="true" />
      </NumberInputIncrement>
    </NumberInputGroup>
  </NumberInput>
  <ToolbarSeparator />
  <ToolbarInput aria-label="Find" placeholder="Find…" />
</Toolbar>
```

Move into Font size and press Up or Down to change it. In Find, type a word, then move the caret to an edge before that arrow leaves the field.

## Links

`ToolbarLink` joins the same focus order and keeps its link behavior. The demo handles a plain click itself and leaves modified clicks, like opening in a new tab, to the browser.

```svelte title="link.svelte"
<script lang="ts">
  import { CheckIcon, MessageSquareIcon } from "@lucide/svelte"
  import {
    Toolbar,
    ToolbarButton,
    ToolbarLink,
    ToolbarSeparator,
  } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const log = useLog()

  function follow(event: MouseEvent & { currentTarget: HTMLAnchorElement }) {
    if (
      event.button !== 0 ||
      event.metaKey ||
      event.ctrlKey ||
      event.shiftKey ||
      event.altKey
    ) {
      return
    }
    event.preventDefault()
    log(`Opened ${event.currentTarget.textContent}`)
  }
</script>

<Toolbar aria-label="Pull request">
  <ToolbarButton aria-label="Approve" onclick={() => log("Approved")}>
    <CheckIcon aria-hidden="true" />
  </ToolbarButton>
  <ToolbarButton aria-label="Comment" onclick={() => log("Comment")}>
    <MessageSquareIcon aria-hidden="true" />
  </ToolbarButton>
  <ToolbarSeparator />
  <ToolbarLink href="#files" onclick={follow}>Files Changed</ToolbarLink>
  <ToolbarLink href="#checks" onclick={follow}>Checks</ToolbarLink>
</Toolbar>
```

Arrow to Files Changed and press Enter. The console logs the page it opened.

## Vertical

Set `orientation="vertical"` to move with Up and Down, like a canvas tool palette. Separators turn to run across the toolbar.

```svelte title="vertical.svelte"
<script lang="ts">
  import {
    MousePointer2Icon,
    PencilIcon,
    SquareIcon,
    Trash2Icon,
    TypeIcon,
  } from "@lucide/svelte"
  import { Toggle, ToggleGroup, ToggleIcon } from "@ui-sdk/svelte/toggle"
  import {
    Toolbar,
    ToolbarButton,
    ToolbarSeparator,
  } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const tools = [
    { value: "select", label: "Select", icon: MousePointer2Icon },
    { value: "draw", label: "Draw", icon: PencilIcon },
    { value: "shape", label: "Shape", icon: SquareIcon },
    { value: "text", label: "Text", icon: TypeIcon },
  ]

  const log = useLog()
</script>

<Toolbar orientation="vertical" aria-label="Canvas">
  <ToggleGroup
    defaultValue={["select"]}
    deselectable={false}
    orientation="vertical"
    aria-label="Tool"
    onValueChange={([value]) =>
      log(`${tools.find((tool) => tool.value === value)?.label} tool`)}
  >
    {#each tools as { value, label, icon: Icon } (value)}
      <Toggle {value} aria-label={label}>
        <ToggleIcon>
          <Icon />
        </ToggleIcon>
      </Toggle>
    {/each}
  </ToggleGroup>
  <ToolbarSeparator />
  <ToolbarButton aria-label="Delete" onclick={() => log("Deleted selection")}>
    <Trash2Icon aria-hidden="true" />
  </ToolbarButton>
</Toolbar>
```

Use Up and Down to move between the tools and Delete. Space picks the focused tool.

## Disabled

A disabled item stays reachable with `aria-disabled`, so people can find it, but it ignores presses, including keys meant for a component rendered through it. Set `focusableWhenDisabled` to `false` to skip it instead.

```svelte title="disabled.svelte"
<script lang="ts">
  import { Toolbar, ToolbarButton } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const log = useLog()
</script>

<Toolbar aria-label="Message">
  <ToolbarButton onclick={() => log("Reply")}>Reply</ToolbarButton>
  <ToolbarButton disabled onclick={() => log("Archive")}>Archive</ToolbarButton>
  <ToolbarButton
    disabled
    focusableWhenDisabled={false}
    onclick={() => log("Delete")}>Delete</ToolbarButton
  >
</Toolbar>
```

Arrow from Reply to Archive, which is disabled but focusable. Delete is skipped. Only Reply logs a press.

Set `disabled` on the toolbar to disable every item. The toolbar keeps its tab stop, and its fields can't be edited.

```svelte
<Toolbar disabled aria-label="Formatting">
  <ToolbarButton>Bold</ToolbarButton>
  <ToolbarInput aria-label="Find" />
</Toolbar>
```

## Loop

Arrow keys wrap from the last item to the first by default. Set `loop` to `false` to stop at the ends.

```svelte title="loop.svelte"
<script lang="ts">
  import { Toolbar, ToolbarButton } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const log = useLog()
</script>

<Toolbar loop={false} aria-label="Pages">
  {#each ["First", "Previous", "Next", "Last"] as label (label)}
    <ToolbarButton onclick={() => log(`${label} page`)}>{label}</ToolbarButton>
  {/each}
</Toolbar>
```

Focus Last and press Right. Focus stays there; Home still moves to First.

## Controller

Create a controller with `createToolbarController` and pass it as `controller`. Its `focus()` moves focus to the item focused last, like the Alt+F10 shortcut editors give their toolbars. Create one controller per toolbar.

```svelte title="controller.svelte"
<script lang="ts">
  import { BoldIcon, ItalicIcon, LinkIcon } from "@lucide/svelte"
  import { useShortcut } from "@ui-sdk/svelte/shortcuts"
  import {
    createToolbarController,
    Toolbar,
    ToolbarButton,
  } from "@ui-sdk/svelte/toolbar"
  import { useLog } from "./log"

  const actions = [
    { label: "Bold", icon: BoldIcon },
    { label: "Italic", icon: ItalicIcon },
    { label: "Insert link", icon: LinkIcon },
  ]

  const log = useLog()
  const toolbar = createToolbarController()
  useShortcut("alt+f10", () => toolbar.focus(), { allowInInput: true })
</script>

<div data-slot="stack">
  <div data-slot="controls">
    <button type="button" onclick={() => toolbar.focus()}>Focus Toolbar</button>
  </div>
  <Toolbar controller={toolbar} aria-label="Notes formatting">
    {#each actions as { label, icon: Icon } (label)}
      <ToolbarButton aria-label={label} onclick={() => log(label)}>
        <Icon aria-hidden="true" />
      </ToolbarButton>
    {/each}
  </Toolbar>
  <textarea aria-label="Notes" placeholder="Type, then press Alt+F10"
  ></textarea>
</div>
```

Type in Notes, then press Alt+F10 or choose Focus Toolbar. Press a toolbar button and the console logs it.

## Custom Rendering

Render a component through `ToolbarButton` or `ToolbarInput` with a `render` snippet to combine their behavior, as the menu, select and number input examples do. The snippet must spread its `props` onto the inner component. [Parts and Composition](/docs/svelte/parts-and-composition) explains the rendering contract.

## Styling

Use `data-orientation` on the root for a row or a column, and on a separator for the way its line runs, across the toolbar. Use `data-disabled` to style unavailable items and groups. Toggles inside keep their own `data-pressed`.

Each part has a `data-slot`, such as `toolbar-button`, and its state attributes are listed under [Data Attributes](#data-attributes).

## Data Attributes

### Toolbar
`[data-slot="toolbar"]`

- `data-orientation` (`horizontal`, `vertical`): The toolbar's orientation. On a separator, which way the line runs: across the toolbar.
- `data-disabled`: Present when disabled, by the item itself, its group or the toolbar.

### ToolbarButton
`[data-slot="toolbar-button"]`

- `data-disabled`: Present when disabled, by the item itself, its group or the toolbar.
- `data-orientation` (`horizontal`, `vertical`): The toolbar's orientation. On a separator, which way the line runs: across the toolbar.

### ToolbarLink
`[data-slot="toolbar-link"]`

- `data-disabled`: Present when disabled, by the item itself, its group or the toolbar.
- `data-orientation` (`horizontal`, `vertical`): The toolbar's orientation. On a separator, which way the line runs: across the toolbar.

### ToolbarInput
`[data-slot="toolbar-input"]`

- `data-disabled`: Present when disabled, by the item itself, its group or the toolbar.
- `data-orientation` (`horizontal`, `vertical`): The toolbar's orientation. On a separator, which way the line runs: across the toolbar.

### ToolbarGroup
`[data-slot="toolbar-group"]`

- `data-disabled`: Present when disabled, by the item itself, its group or the toolbar.
- `data-orientation` (`horizontal`, `vertical`): The toolbar's orientation. On a separator, which way the line runs: across the toolbar.

### ToolbarSeparator
`[data-slot="toolbar-separator"]`

- `data-orientation` (`horizontal`, `vertical`): The toolbar's orientation. On a separator, which way the line runs: across the toolbar.

## API Reference

### Toolbar

- `orientation?` (`ToolbarOrientation`): Which arrow keys move between items: left and right, or up and down.
- `loop?` (`boolean`): Whether arrow keys wrap from the last item to the first.
- `disabled?` (`boolean`): Stops every item from being used. Items stay focusable with `aria-disabled`, so the toolbar keeps its tab stop and stays discoverable.
- `controller?` (`{ focus: () => void; }`): Focuses the toolbar from anywhere. Create one with `createToolbarController()`.
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.

### ToolbarButton

- `disabled?` (`boolean`): Stops the item from being used. It stays focusable with `aria-disabled`, so arrow keys still reach it and a tooltip can say why.
- `focusableWhenDisabled?` (`boolean`): Whether a disabled item stays reachable with arrow keys. Turn it off to skip it, natively disabled.

### ToolbarInput

- `disabled?` (`boolean`): Stops the item from being used. It stays focusable with `aria-disabled`, so arrow keys still reach it and a tooltip can say why.
- `focusableWhenDisabled?` (`boolean`): Whether a disabled item stays reachable with arrow keys. Turn it off to skip it, natively disabled.

### ToolbarGroup

- `disabled?` (`boolean`): Disables every item in the group. They stay focusable with `aria-disabled`.

`ToolbarLink` and `ToolbarSeparator` take no props of their own.

### State

What render functions and state-based class names receive on `ToolbarButton` and `ToolbarInput`:

- `disabled` (`boolean`): Whether the item is disabled, by itself, its group or the toolbar.
- `orientation` (`ToolbarOrientation`): The toolbar's `orientation` option: `"vertical"`, or `"horizontal"` for anything else.

On `ToolbarLink`:

- `disabled` (`boolean`): Whether the link is disabled, by its group or the toolbar.
- `orientation` (`ToolbarOrientation`): The toolbar's `orientation` option: `"vertical"`, or `"horizontal"` for anything else.

On `ToolbarSeparator`:

- `orientation` (`ToolbarOrientation`): Which way the line runs: across the toolbar, so vertical in a horizontal toolbar.

### Controller

- `focus()` (`() => void`): Moves focus to the item that holds the toolbar's tab stop: the one focused last, or the first.

## Accessibility

The toolbar follows the [WAI-ARIA toolbar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/). The root has the `toolbar` role and `aria-orientation`, and separators have the `separator` role.

### Keyboard

The toolbar has one tab stop, a roving focus. Tab enters on the first enabled item, then returns to the item focused last. The next Tab leaves the toolbar.

| Key                        | Action                                          |
| -------------------------- | ----------------------------------------------- |
| `ArrowRight` / `ArrowLeft` | Move focus in a horizontal toolbar              |
| `ArrowDown` / `ArrowUp`    | Move focus in a vertical toolbar                |
| `Home` / `End`             | Focus the first or last item, except in a field |
| `Enter` / `Space`          | Activate a button or toggle, or open its popup  |

Controls keep the keys they use: Down opens a menu, and Up and Down step a number input. Moving focus never presses a toggle. In right-to-left text, Left and Right swap.

### Tested Behavior

Every line below is an automated test.

- The toolbar is named and has a horizontal orientation.
- Tab lands on the first item, and Tab again leaves the toolbar.
- Shift+Tab from after the toolbar lands on its tab stop.
- ArrowRight moves through every item, into toggle groups and out of fields.
- ArrowRight on the last item wraps to the first, and ArrowLeft back.
- Arrow keys never press a toggle, and Space and Enter do.
- Tab comes back to the item focused last.
- A toggle group in the toolbar is no tab stop of its own.
- A disabled button stays reachable, says it's disabled and does nothing.
- A button runs its action when pressed.
- A menu trigger rendered through a button opens with ArrowDown, and arrows still move past it.
- Choosing from a menu in the toolbar returns focus to its trigger.
- A text field keeps Home and End for its caret.
- Up and Down step a number field in the toolbar.
- Separators run across the toolbar.
- A group of buttons is named.
- A link in the toolbar follows its href with Enter.
- With loop off, arrows stop at the ends.
- A disabled toolbar keeps one tab stop, and every item says it's disabled.

**Vertical**

- ArrowDown and ArrowUp move between items, and Left and Right don't.
- Separators run across a vertical toolbar.

**Disabled groups**

- Toggles and toggle groups in a disabled group are disabled.
- A link in a disabled group is disabled, and follows its href once enabled.
- Enabling a group enables the toggles and buttons in it.

Runs in React, Vue and web components.
