Toolbar

Compose buttons, links and toggle groups with shared keyboard navigation.

<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>
vue

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'
ts

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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>
vue

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.

KeyBehavior
Tab / Shift + TabEnter 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 / EndMove to the first / last enabled item
Enter / SpaceActivate 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.