<script setup lang="ts">
import { ref } from 'vue'
import { Bold, Italic, Underline, Undo2, Redo2 } from '@lucide/vue'
import {
Toolbar,
ToolbarButton,
ToolbarToggleGroup,
ToolbarToggleItem,
ToolbarSeparator,
} from '@hina-ui/vue'
const formats = ref(['bold'])
</script>
<template>
<Toolbar label="Text tools" size="sm">
<ToolbarButton label="Undo"><Undo2 /></ToolbarButton>
<ToolbarButton label="Redo" disabled><Redo2 /></ToolbarButton>
<ToolbarSeparator />
<ToolbarToggleGroup v-model="formats" type="multiple" label="Formatting">
<ToolbarToggleItem value="bold" label="Bold"><Bold /></ToolbarToggleItem>
<ToolbarToggleItem value="italic" label="Italic"><Italic /></ToolbarToggleItem>
<ToolbarToggleItem value="underline" label="Underline"><Underline /></ToolbarToggleItem>
</ToolbarToggleGroup>
</Toolbar>
</template>
Usage
Toolbar provides toolbar semantics, sizing and arrow-key navigation. ToolbarButton performs an action, ToolbarLink renders a link, and ToolbarSeparator separates controls. Controls reuse Button styling.
import {
Toolbar,
ToolbarButton,
ToolbarLink,
ToolbarToggleGroup,
ToolbarToggleItem,
ToolbarSeparator,
} from '@hina-ui/vue'
Name the toolbar with label or aria-labelledby. Text buttons use the default slot, with #icon and #trailing for adjacent content. For icon-only controls, set label and place the icon in the default slot. This provides square sizing, an accessible name, and a tooltip inside TooltipProvider, matching IconButton.
No action yet
<script setup lang="ts">
import { ref } from 'vue'
import { Copy, Download, RotateCcw } from '@lucide/vue'
import { Toolbar, ToolbarButton, ToolbarLink, ToolbarSeparator, Stack, Text } from '@hina-ui/vue'
const action = ref('No action yet')
</script>
<template>
<Stack align="start" gap="sm">
<Toolbar label="Actions" size="sm">
<ToolbarButton @click="action = 'Copy'">
<template #icon><Copy /></template>
Copy
</ToolbarButton>
<ToolbarButton @click="action = 'Download'">
<template #icon><Download /></template>
Download
</ToolbarButton>
<ToolbarButton @click="action = 'Reset'">
<template #icon><RotateCcw /></template>
Reset
</ToolbarButton>
<ToolbarSeparator />
<ToolbarLink href="#api">API</ToolbarLink>
</Toolbar>
<Text size="sm" tone="muted" aria-live="polite">{{ action }}</Text>
</Stack>
</template>
Examples
Single and multiple selection
ToolbarToggleGroup defaults to type="single", binding a string or undefined. Activating the selected item clears it. With type="multiple", bind an array of strings; each item toggles independently. Use default-value for uncontrolled initial state.
Each ToolbarToggleItem needs a unique value within its group. Arrow keys only move focus; click, Enter or Space changes selection.
Formats: bold · Alignment: start
<script setup lang="ts">
import { ref } from 'vue'
import { Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight } from '@lucide/vue'
import {
Toolbar,
ToolbarToggleGroup,
ToolbarToggleItem,
ToolbarSeparator,
Stack,
Text,
} from '@hina-ui/vue'
const formats = ref(['bold'])
const alignment = ref<string | undefined>('start')
</script>
<template>
<Stack align="start" gap="sm">
<Toolbar label="Toggle groups" size="sm">
<ToolbarToggleGroup v-model="formats" type="multiple" label="Formatting">
<ToolbarToggleItem value="bold" label="Bold"><Bold /></ToolbarToggleItem>
<ToolbarToggleItem value="italic" label="Italic"><Italic /></ToolbarToggleItem>
<ToolbarToggleItem value="underline" label="Underline"><Underline /></ToolbarToggleItem>
</ToolbarToggleGroup>
<ToolbarSeparator />
<ToolbarToggleGroup v-model="alignment" label="Alignment">
<ToolbarToggleItem value="start" label="Align start"><AlignLeft /></ToolbarToggleItem>
<ToolbarToggleItem value="center" label="Center"><AlignCenter /></ToolbarToggleItem>
<ToolbarToggleItem value="end" label="Align end"><AlignRight /></ToolbarToggleItem>
</ToolbarToggleGroup>
</Toolbar>
<Text size="sm" tone="muted">
Formats: {{ formats.join(', ') || 'None' }} · Alignment: {{ alignment ?? 'None' }}
</Text>
</Stack>
</template>
Vertical
orientation="vertical" stacks controls and uses the up/down arrow keys. Separators turn automatically. Set each control’s side to change tooltip placement.
<script setup lang="ts">
import { ref } from 'vue'
import { MousePointer2, Hand, Type, Plus } from '@lucide/vue'
import {
Toolbar,
ToolbarButton,
ToolbarToggleGroup,
ToolbarToggleItem,
ToolbarSeparator,
} from '@hina-ui/vue'
const tool = ref<string | undefined>('pointer')
</script>
<template>
<Toolbar label="Vertical tools" orientation="vertical" size="sm">
<ToolbarToggleGroup v-model="tool" label="Tools">
<ToolbarToggleItem value="pointer" label="Select" side="right">
<MousePointer2 />
</ToolbarToggleItem>
<ToolbarToggleItem value="hand" label="Move" side="right"><Hand /></ToolbarToggleItem>
<ToolbarToggleItem value="text" label="Text" side="right"><Type /></ToolbarToggleItem>
</ToolbarToggleGroup>
<ToolbarSeparator />
<ToolbarButton label="Add" side="right"><Plus /></ToolbarButton>
</Toolbar>
</template>
Appearance
primary has a border and surface background, secondary uses an inset background, and bare removes the background, border and padding.
primary
secondary
bare
<script setup lang="ts">
import { Copy, Scissors, ClipboardPaste } from '@lucide/vue'
import { Toolbar, ToolbarButton, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Stack align="start">
<Stack
v-for="variant in ['primary', 'secondary', 'bare'] as const"
:key="variant"
align="start"
gap="xs"
>
<Text size="sm" tone="muted">{{ variant }}</Text>
<Toolbar :variant="variant" :label="variant" size="sm">
<ToolbarButton label="Cut"><Scissors /></ToolbarButton>
<ToolbarButton label="Copy"><Copy /></ToolbarButton>
<ToolbarButton label="Paste"><ClipboardPaste /></ToolbarButton>
</Toolbar>
</Stack>
</Stack>
</template>
Sizes
Buttons, links and toggle items inherit the toolbar’s size. Individual controls can override it. Gaps and control heights follow density tokens.
sm
md
lg
<script setup lang="ts">
import { Copy, Scissors, ClipboardPaste } from '@lucide/vue'
import { Toolbar, ToolbarButton, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Stack align="start">
<Stack v-for="size in ['sm', 'md', 'lg'] as const" :key="size" align="start" gap="xs">
<Text size="sm" tone="muted">{{ size }}</Text>
<Toolbar :size="size" :label="size">
<ToolbarButton label="Cut"><Scissors /></ToolbarButton>
<ToolbarButton label="Copy"><Copy /></ToolbarButton>
<ToolbarButton label="Paste"><ClipboardPaste /></ToolbarButton>
</Toolbar>
</Stack>
</Stack>
</template>
Disabled
Toolbar.disabled disables all controls. ToolbarToggleGroup.disabled disables its toggle items, while an item’s disabled only affects itself. Arrow-key navigation skips disabled controls. loading also disables the control.
<script setup lang="ts">
import { ref } from 'vue'
import { Bold, Italic, Undo2, Redo2 } from '@lucide/vue'
import {
Toolbar,
ToolbarButton,
ToolbarToggleGroup,
ToolbarToggleItem,
ToolbarSeparator,
Stack,
FormField,
Switch,
} from '@hina-ui/vue'
const disabled = ref(false)
const groupDisabled = ref(false)
const formats = ref(['bold'])
</script>
<template>
<Stack align="start">
<FormField label="Disable all" orientation="horizontal">
<Switch v-model="disabled" />
</FormField>
<FormField label="Disable toggle group" orientation="horizontal">
<Switch v-model="groupDisabled" />
</FormField>
<Toolbar label="Disabled state" :disabled="disabled" size="sm">
<ToolbarButton label="Undo"><Undo2 /></ToolbarButton>
<ToolbarButton label="Redo" disabled><Redo2 /></ToolbarButton>
<ToolbarSeparator />
<ToolbarToggleGroup
v-model="formats"
type="multiple"
label="Formatting"
:disabled="groupDisabled"
>
<ToolbarToggleItem value="bold" label="Bold"><Bold /></ToolbarToggleItem>
<ToolbarToggleItem value="italic" label="Italic"><Italic /></ToolbarToggleItem>
</ToolbarToggleGroup>
</Toolbar>
</Stack>
</template>
Composing controls
Use ToolbarButton as the trigger of DropdownMenu or Popover to preserve both the overlay’s and toolbar’s keyboard behavior.
as-child merges behavior and attributes into one child control without nesting buttons. This example reuses Toggle. Custom children must forward attributes and events to their focusable element. Set disabled and loading states on ToolbarButton so it also leaves arrow-key navigation.
No action yet
<script setup lang="ts">
import { ref } from 'vue'
import { Bold, Copy, Ellipsis, Trash2 } from '@lucide/vue'
import {
Toolbar,
ToolbarButton,
ToolbarSeparator,
Toggle,
DropdownMenu,
DropdownMenuItem,
Stack,
Text,
} from '@hina-ui/vue'
const bold = ref(false)
const action = ref('No action yet')
</script>
<template>
<Stack align="start" gap="sm">
<Toolbar label="Composed controls" size="sm">
<ToolbarButton as-child>
<Toggle v-model="bold" label="Bold" size="sm">
<template #icon><Bold /></template>
</Toggle>
</ToolbarButton>
<ToolbarButton label="Copy" @click="action = 'Copy'"><Copy /></ToolbarButton>
<ToolbarSeparator />
<DropdownMenu label="More actions">
<ToolbarButton label="More"><Ellipsis /></ToolbarButton>
<template #content>
<DropdownMenuItem @select="action = 'Copy'">
<template #icon><Copy /></template>
Copy
</DropdownMenuItem>
<DropdownMenuItem tone="danger" @select="action = 'Delete'">
<template #icon><Trash2 /></template>
Delete
</DropdownMenuItem>
</template>
</DropdownMenu>
</Toolbar>
<Text size="sm" tone="muted" aria-live="polite">{{ action }}</Text>
</Stack>
</template>
RTL
dir="rtl" changes layout and left/right arrow behavior together. When omitted, direction comes from ConfigProvider or the nearest ancestor’s dir.
<script setup lang="ts">
import { Toolbar, ToolbarButton, ToolbarSeparator, ToolbarLink } from '@hina-ui/vue'
</script>
<template>
<Toolbar label="RTL toolbar" dir="rtl" size="sm">
<ToolbarButton>First</ToolbarButton>
<ToolbarButton>Second</ToolbarButton>
<ToolbarSeparator />
<ToolbarLink href="#api">API</ToolbarLink>
</Toolbar>
</template>
Behavior and accessibility
The container has role="toolbar", toggle groups use role="group", and toggle items expose aria-pressed. Buttons default to type="button" and do not submit a surrounding form.
| Key | Behavior |
|---|---|
| Tab / Shift + Tab | Enter or leave the toolbar; re-entry restores the last focused item |
| ← / → | Move focus in a horizontal toolbar, reversed in RTL |
| ↑ / ↓ | Move focus in a vertical toolbar |
| Home / End | Move to the first / last enabled item |
| Enter / Space | Activate buttons or links, or change toggle state |
loop defaults to true, allowing navigation between the last and first items. Horizontal controls can wrap naturally. Arrow keys continue to follow DOM order rather than a two-dimensional grid.
Controls that need the same arrow keys for their own operation, such as text inputs or Slider, should retain their own keyboard handling. Do not use ToolbarButton to add them to this arrow-key sequence.
API
Toolbar
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Accessible name; alternatively pass aria-labelledby |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Layout and navigation direction |
dir | 'ltr' | 'rtl' | Inherited | Reading direction |
loop | boolean | true | Wrap focus at either end |
disabled | boolean | false | Disable all controls |
size | 'sm' | 'md' | 'lg' | 'md' | Control size |
variant | 'primary' | 'secondary' | 'bare' | 'primary' | Container appearance |
class | string | — | Root classes |
Place toolbar parts in the default slot. Native attributes are forwarded to the root.
ToolbarButton / ToolbarLink / ToolbarToggleItem
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Accessible name and tooltip for icon-only controls |
tooltip | boolean | true | Show a tooltip with label and TooltipProvider |
side | 'top' | 'right' | 'bottom' | 'left' | 'top' | Tooltip placement |
size | 'sm' | 'md' | 'lg' | Inherited | Control size |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'ghost' | Button appearance |
tone | 'accent' | 'neutral' | 'danger' | 'neutral' | Color tone |
disabled | boolean | false | Disable this control |
loading | boolean | false | Show loading state and disable |
ripple | boolean | true | Press ripple |
as | string | Component | 'button', or 'a' for links | Rendered element |
asChild | boolean | false | Merge into a single child |
class | string | — | Control classes |
ToolbarLink also accepts href, target and rel. ToolbarToggleItem requires value: string and must be inside ToolbarToggleGroup. The default, #icon and #trailing slots match Button. Native attributes and events are forwarded to the control.
ToolbarToggleGroup
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | string | string[] | undefined | — | Current selection |
defaultValue | string | string[] | — | Uncontrolled initial selection |
type | 'single' | 'multiple' | 'single' | Selection mode |
label | string | — | Group name |
disabled | boolean | false | Disable the group’s toggle items |
class | string | — | Group classes |
Place toggle items in the default slot. update:modelValue fires when selection changes. Items remain part of the outer Toolbar’s single focus sequence.
ToolbarSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
decorative | boolean | true | Whether the separator is decorative |
class | string | — | Separator classes |
Separators are perpendicular to the toolbar’s orientation and do not receive keyboard focus.