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.
<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.Rootanaria-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’sid.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.Inputin 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