# Toolbar

A row of buttons, links and controls behind a single tab stop.

Toolbar gathers buttons, links and controls into one group that the keyboard
enters once, with the arrow keys moving between the items inside it. Useful for
a formatting bar above an editor, or anywhere a cluster of controls would
otherwise cost a keyboard user one `Tab` each.

::component-preview{name="ToolbarPreview"}
```vue
<template>
  <Toolbar.Root :class="toolbar" aria-label="Text formatting">
    <Toolbar.Button
      v-for="format in formats"
      :key="format.label"
      :aria-label="format.label"
      :aria-pressed="format.pressed"
      :class="[button, format.glyphClass]"
      @click="format.pressed = !format.pressed"
    >
      {{ format.glyph }}
    </Toolbar.Button>

    <Toolbar.Separator :class="separator" />

    <Toolbar.Group :class="group" aria-label="Alignment">
      <Toolbar.Button
        v-for="option in alignments"
        :key="option.value"
        :aria-label="option.label"
        :aria-pressed="alignment === option.value"
        :class="button"
        @click="alignment = option.value"
      >
        <svg class="size-4.5" viewBox="0 0 18 18" aria-hidden="true">
          <path
            :d="option.path"
            fill="none"
            stroke="currentColor"
            stroke-width="1.5"
            stroke-linecap="round"
          />
        </svg>
      </Toolbar.Button>
    </Toolbar.Group>

    <Toolbar.Link :class="link" href="#accessibility">Help</Toolbar.Link>
  </Toolbar.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Toolbar } from '@maas/mirror/vue'

const toolbar =
  'border-surface rounded-[calc(var(--radius-component-md)+0.375rem+1px)] flex w-full max-w-md items-center gap-1 border p-1.5'

const button = [
  'inline-flex size-9 shrink-0 items-center justify-center',
  'rounded-component-md border-2 border-[transparent] type-component-md',
  'text-primary-solid transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-subtle active:bg-primary-subtle-active',
  'aria-[pressed=true]:bg-primary-solid aria-[pressed=true]:text-primary-on-solid',
  'aria-[pressed=true]:hover:bg-primary-solid-hover',
  'data-[disabled=true]:text-disabled-on-subtle',
].join(' ')

const separator = 'border-surface mx-1 h-6 self-center border-l'

const group = 'flex items-center gap-1'

const link = [
  'rounded-component-md type-component-sm text-surface-link ml-auto',
  'border-2 border-[transparent] px-2.5 py-1.5 no-underline',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:text-surface-link-hover active:text-surface-link-active',
].join(' ')

const formats = ref([
  { label: 'Bold', glyph: 'B', glyphClass: 'font-bold', pressed: true },
  { label: 'Italic', glyph: 'I', glyphClass: 'italic', pressed: false },
  { label: 'Underline', glyph: 'U', glyphClass: 'underline', pressed: false },
])

const alignments = [
  { value: 'left', label: 'Align left', path: 'M3 5h12M3 9h7M3 13h10' },
  { value: 'center', label: 'Align centre', path: 'M3 5h12M6 9h6M4 13h10' },
  { value: 'right', label: 'Align right', path: 'M3 5h12M8 9h7M5 13h10' },
]

const alignment = ref('left')
</script>
```
::

## Usage guidelines

- Give `Toolbar.Root` an `aria-label`, since a screen reader announces the
  toolbar by that name before it announces anything inside it.
- Keep every item inside `Toolbar.Root`. The arrow-key order is the root’s own
  subtree, so an item rendered elsewhere has nothing to move within, even when
  it carries the toolbar’s `id`.
- The toolbar does not decide what a button does. Bold, italic and the rest keep
  their own pressed state and their own `aria-pressed`, as the demo above does.
- Use at most one `Toolbar.Input` in a horizontal toolbar, and put it last,
  since the left and right arrow keys have to be shared between the caret and
  the toolbar.

## Anatomy

Assemble the parts inside one root.

::component-anatomy
---
parts:
  - name: Toolbar.Root
    required: true
    description: 'Renders a <div role="toolbar"> and owns the single tab stop.'
    children:
      - name: Toolbar.Button
        description: 'Renders a <button>. One arrow-key stop.'
      - name: Toolbar.Link
        description: 'Renders an <a>. One arrow-key stop.'
      - name: Toolbar.Input
        description: 'Renders an <input>. One arrow-key stop that keeps the caret keys.'
      - name: Toolbar.Group
        description: 'Renders a <div role="group"> and passes its disabled state down.'
      - name: Toolbar.Separator
        description: 'Renders a <div role="separator">. Not an arrow-key stop.'
---
::

```vue
<script setup lang="ts">
import { Toolbar } from '@maas/mirror/vue'
</script>

<template>
  <Toolbar.Root aria-label="Text formatting">
    <Toolbar.Button>B</Toolbar.Button>
    <Toolbar.Button>I</Toolbar.Button>
    <Toolbar.Separator />
    <Toolbar.Group aria-label="Alignment">
      <Toolbar.Button>Left</Toolbar.Button>
      <Toolbar.Button>Right</Toolbar.Button>
    </Toolbar.Group>
    <Toolbar.Link href="/help">Help</Toolbar.Link>
  </Toolbar.Root>
</template>
```

## Examples

### Moving between items

`orientation` decides which arrow keys move, and is written to
`data-orientation` on every part, so lay the toolbar out from that attribute
rather than from a class of your own.

```vue
<template>
  <Toolbar.Root :options="{ orientation: 'vertical' }" aria-label="Tools">
    <Toolbar.Button>Pen</Toolbar.Button>
    <Toolbar.Button>Eraser</Toolbar.Button>
  </Toolbar.Root>
</template>
```

Arrow keys wrap at the ends. Set `loopFocus` to `false` to stop there instead,
and `dir` to mirror the horizontal keys. Unset, `dir` follows a
`DirectionProvider` or the surrounding `dir` attribute, as described in
[Composition](/components/composition).

```vue
<template>
  <Toolbar.Root :options="{ loopFocus: false, dir: 'rtl' }" />
</template>
```

### Disabling items

`disabled` in the root’s options disables every item inside it, and `disabled`
on a `Toolbar.Group` disables the items in that group. An item that sets `disabled`
itself wins over both, so `:disabled="false"` is how one button stays usable in
a disabled group.

```vue
<template>
  <Toolbar.Root :options="{ disabled: true }">
    <Toolbar.Button>Cut</Toolbar.Button>
    <Toolbar.Button :disabled="false">Undo</Toolbar.Button>
  </Toolbar.Root>
</template>
```

A disabled button keeps its place in the arrow-key order and carries
`aria-disabled` rather than the native `disabled` attribute, so a screen reader
still reaches it and says why it cannot be used. Set `focusableWhenDisabled` to
`false` where you would rather it were skipped entirely, and the native
attribute comes back with it.

```vue
<template>
  <Toolbar.Button :focusable-when-disabled="false" disabled>
    Paste
  </Toolbar.Button>
</template>
```

`Toolbar.Link` never takes the disabled state, so wrap it in `v-if` where it
should not be available.

### Separating items

`Toolbar.Separator` runs across the toolbar rather than along it, so it reports
the opposite orientation to everything else. Draw it from `data-orientation` and
one rule covers both.

```vue
<template>
  <Toolbar.Separator class="separator" />
</template>

<style>
.separator[data-orientation='vertical'] {
  width: 1px;
  align-self: stretch;
  background: var(--app-color-surface-border);
}

.separator[data-orientation='horizontal'] {
  height: 1px;
  background: var(--app-color-surface-border);
}
</style>
```

It is not an arrow-key stop, so the keys pass straight over it.

### An input in the toolbar

`Toolbar.Input` renders a native input and takes a `v-model`. The arrow keys
move the caret while there is text left to travel, and only reach the toolbar
once the caret sits collapsed against the edge it is heading for. `Home` and
`End` always stay with the text.

```vue
<template>
  <Toolbar.Root aria-label="Formatting">
    <Toolbar.Button>B</Toolbar.Button>
    <Toolbar.Input v-model="size" aria-label="Font size" />
  </Toolbar.Root>
</template>

<script setup lang="ts">
const size = ref('16')
</script>
```

That sharing is why one input, placed last, is the sensible limit in a
horizontal toolbar. A vertical toolbar navigates on the up and down arrows,
which the caret never uses, so the same limit does not apply.

### Styling from state

Every part writes its state to `data-*` attributes, and the item holding the tab
stop carries `data-highlighted`, so a toolbar can show where the keyboard is
without a class of your own.

```vue
<template>
  <Toolbar.Button class="button">B</Toolbar.Button>
</template>

<style>
.button[data-highlighted='true'] {
  background: var(--app-color-primary-bg-subtle);
}

.button[data-disabled='true'][data-focusable='true'] {
  opacity: 0.4;
}
</style>
```

### Nesting another roving group

`Toolbar.Root` is the roving-focus group, and any descendant that registers as a
roving-focus item joins the same order. In practice that means a nested group
renders its items through `Toolbar.Button` with `asChild`, and the toolbar keeps
one tab stop across both.

```vue
<template>
  <Toolbar.Root aria-label="Formatting">
    <Toolbar.Button>B</Toolbar.Button>
    <Toolbar.Group aria-label="Alignment">
      <Toolbar.Button as-child>
        <OtherGroupItem value="left">Left</OtherGroupItem>
      </Toolbar.Button>
    </Toolbar.Group>
  </Toolbar.Root>
</template>
```

### Reaching the toolbar from elsewhere

Give the root an `id` and `useMirrorToolbar(id)` reads the same state from
anywhere in the app, which is how a menu item or a shortcut hands focus back to
the toolbar.

```vue
<template>
  <Toolbar.Root id="editor-toolbar" aria-label="Formatting" />
</template>

<script setup lang="ts">
const { orientation, currentItemId, focusFirst } =
  useMirrorToolbar('editor-toolbar')
</script>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorToolbar(id)` as the
programmatic API. Every part takes `id` to resolve a `Toolbar` it is not nested
inside, though the items themselves have to stay in the root’s subtree to take
part in the arrow-key order.

### `Toolbar.Root`

Renders a `<div role="toolbar">`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: '[The instance ID](#reaching-the-toolbar-from-elsewhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: ToolbarOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: orientation
        description: 'The [arrow-key axis](#moving-between-items).'
      - label: '''horizontal'' | ''vertical'''
      - label: '''horizontal'''
  - items:
      - label: dir
        description: 'Direction for the [horizontal arrow keys](#moving-between-items).'
      - label: '''ltr'' | ''rtl'''
      - label: inherited
        plaintext: true
  - items:
      - label: loopFocus
        description: Arrow keys wrap at the ends.
      - label: boolean
      - label: 'true'
  - items:
      - label: disabled
        description: 'Disables [every item](#disabling-items) that does not decide for itself.'
      - label: boolean
      - label: 'false'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
---
::

### `Toolbar.Button`

Renders a `<button>` and takes one place in the arrow-key order.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID of the `Toolbar` this belongs to.
      - label: string
      - label: injected
        plaintext: true
  - items:
      - label: elementId
        description: The button’s own DOM ID, which is also its ID in the arrow-key order.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: disabled
        description: 'Blocks [activation](#disabling-items).'
      - label: boolean
      - label: inherited
        plaintext: true
  - items:
      - label: focusableWhenDisabled
        description: 'Keeps a disabled button [in the arrow-key order](#disabling-items).'
      - label: boolean
      - label: 'true'
  - items:
      - label: type
        description: The `type` attribute, on a native button only.
      - label: '''button'' | ''submit'' | ''reset'''
      - label: '''button'''
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
  - items:
      - label: focusable
        description: Mirrors `data-focusable`.
      - label: boolean
  - items:
      - label: highlighted
        description: Mirrors `data-highlighted`.
      - label: boolean
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
---
::

### `Toolbar.Link`

Renders an `<a>` and takes one place in the arrow-key order. Takes `id`,
`elementId` and the primitive props, and never takes the disabled state.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: highlighted
        description: Mirrors `data-highlighted`.
      - label: boolean
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
---
::

### `Toolbar.Group`

Renders a `<div role="group">`. It is not an arrow-key stop of its own; it
passes its disabled state to the items inside it.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID of the `Toolbar` this belongs to.
      - label: string
      - label: injected
        plaintext: true
  - items:
      - label: disabled
        description: 'Disables [the items](#disabling-items) inside the group.'
      - label: boolean
      - label: inherited
        plaintext: true
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
---
::

### `Toolbar.Separator`

Renders a `<div role="separator">`, oriented across the toolbar rather than
along it. Not an arrow-key stop.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID of the `Toolbar` this belongs to.
      - label: string
      - label: injected
        plaintext: true
  - items:
      - label: orientation
        description: 'Overrides the [orientation](#separating-items) the separator takes from the toolbar.'
      - label: '''horizontal'' | ''vertical'''
      - label: the toolbar’s, turned by a quarter
        plaintext: true
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
---
::

### `Toolbar.Input`

Renders an `<input>` and takes one place in the arrow-key order, keeping the
keys that move its caret.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID of the `Toolbar` this belongs to.
      - label: string
      - label: injected
        plaintext: true
  - items:
      - label: elementId
        description: The input’s own DOM ID, which is also its ID in the arrow-key order.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The value. `v-model`.
      - label: 'string | number | undefined'
      - label: undefined
  - items:
      - label: defaultValue
        description: 'Initial value when [uncontrolled](/components/composition#controlled-and-uncontrolled).'
      - label: 'string | number'
      - label: ''''''
  - items:
      - label: type
        description: The `type` attribute.
      - label: string
      - label: '''text'''
  - items:
      - label: name
        description: Form field name.
      - label: string
      - label: undefined
  - items:
      - label: disabled
        description: 'Blocks [input](#disabling-items).'
      - label: boolean
      - label: inherited
        plaintext: true
  - items:
      - label: focusableWhenDisabled
        description: Keeps a disabled input in the arrow-key order, read-only rather than disabled.
      - label: boolean
      - label: 'true'
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The value changes, controlled or not.
      - label: 'string | number'
---
::

### Composable

`useMirrorToolbar(id)` reaches a `Toolbar` from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: ComputedRef<ToolbarOrientation>
        escape: true
  - items:
      - label: disabled
        description: Mirrors `data-disabled` on the root.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: dir
        description: The resolved text direction.
      - label: ComputedRef<ToolbarTextDirection>
        escape: true
  - items:
      - label: itemIds
        description: Every registered item, in document order.
      - label: 'ComputedRef<Array<string>>'
        escape: true
  - items:
      - label: currentItemId
        description: 'The item holding the tab stop, or `null` before anything has been focused.'
      - label: 'ComputedRef<string | null>'
        escape: true
  - items:
      - label: focusItem
        description: Focuses one item by its ID. Ignores an item that has left the arrow-key order.
      - label: '(itemId: string) => void'
        escape: true
  - items:
      - label: focusFirst
        description: Focuses the first item still taking focus.
      - label: '() => void'
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_toolbar_context
        description: A `Toolbar` part rendered outside `Toolbar.Root` with no `id` of its own.
  - items:
      - label: missing_context
        description: A `Toolbar.Button`, `Toolbar.Link` or `Toolbar.Input` rendered outside the root’s subtree, where there is no arrow-key order to join.
---
::

## Accessibility

The root is a `role="toolbar"` with `aria-orientation`, the group a
`role="group"`, the separator a `role="separator"`, and the whole toolbar is one
tab stop that follows the last item to hold focus.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: Tab
      - label: Moves into the toolbar at the item that last held focus, then out of it entirely.
        plaintext: true
  - items:
      - label: '`ArrowRight` / `ArrowLeft`'
        plaintext: true
      - label: 'Horizontal toolbar only. Moves to the next or previous item, swapped under `dir="rtl"`. Wraps while `loopFocus` is on.'
        plaintext: true
  - items:
      - label: '`ArrowDown` / `ArrowUp`'
        plaintext: true
      - label: Vertical toolbar only. Moves to the next or previous item, with the same wrapping.
        plaintext: true
  - items:
      - label: '`Home` / `End`'
        plaintext: true
      - label: Moves to the first or last item, in either orientation, ignoring `loopFocus`. Inside a `Toolbar.Input` they move the caret instead.
        plaintext: true
  - items:
      - label: '`Enter` / `Space`'
        plaintext: true
      - label: Activates the focused button, as a button normally would. A non-native button gets the same two keys supplied for it.
        plaintext: true
---
::

A disabled item stays in the order by default and carries `aria-disabled`, so it
is announced rather than skipped in silence. `Toolbar.Root` itself is
`tabindex="-1"`, which is what keeps the toolbar to one tab stop no matter how
many items it holds.
