Skip to content

Toolbar

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

View source View as Markdown

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.

App.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/Toolbar'

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 center', 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.must
  • 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.must
  • 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.should
  • 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.must