Menubar

A row of menus, like the menu bar of a desktop app.

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Kbd,
    Menubar,
    MenubarCheckboxItem,
    MenubarItem,
    MenubarMenu,
    MenubarRadioGroup,
    MenubarRadioItem,
    MenubarSeparator,
    MenubarSub,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const last = ref('')
  const wrap = ref(true)
  const numbers = ref(false)
  const theme = ref('system')
</script>

<template>
  <Stack gap="sm" align="start">
    <Menubar label="Editor menu">
      <MenubarMenu label="File">
        <MenubarItem @select="last = 'New'">
          New
          <template #trailing><Kbd>Ctrl N</Kbd></template>
        </MenubarItem>
        <MenubarItem @select="last = 'Open'">
          Open
          <template #trailing><Kbd>Ctrl O</Kbd></template>
        </MenubarItem>
        <MenubarSub label="Open recent">
          <MenubarItem @select="last = 'chapter-1.md'">chapter-1.md</MenubarItem>
          <MenubarItem @select="last = 'outline.md'">outline.md</MenubarItem>
        </MenubarSub>
        <MenubarSeparator />
        <MenubarItem @select="last = 'Save'">
          Save
          <template #trailing><Kbd>Ctrl S</Kbd></template>
        </MenubarItem>
      </MenubarMenu>
      <MenubarMenu label="Edit">
        <MenubarItem @select="last = 'Undo'">
          Undo
          <template #trailing><Kbd>Ctrl Z</Kbd></template>
        </MenubarItem>
        <MenubarItem @select="last = 'Redo'">
          Redo
          <template #trailing><Kbd>Ctrl Y</Kbd></template>
        </MenubarItem>
        <MenubarSeparator />
        <MenubarItem @select="last = 'Find'">
          Find
          <template #trailing><Kbd>Ctrl F</Kbd></template>
        </MenubarItem>
      </MenubarMenu>
      <MenubarMenu label="View">
        <MenubarCheckboxItem v-model:checked="wrap">Word wrap</MenubarCheckboxItem>
        <MenubarCheckboxItem v-model:checked="numbers">Line numbers</MenubarCheckboxItem>
        <MenubarSeparator />
        <MenubarRadioGroup v-model="theme">
          <MenubarRadioItem value="light">Light</MenubarRadioItem>
          <MenubarRadioItem value="dark">Dark</MenubarRadioItem>
          <MenubarRadioItem value="system">System</MenubarRadioItem>
        </MenubarRadioGroup>
      </MenubarMenu>
    </Menubar>
    <Text v-if="last" tone="muted" size="sm">Picked: {{ last }}</Text>
  </Stack>
</template>
vue

Usage

import { Menubar, MenubarMenu, MenubarItem } from '@hina-ui/vue'
ts

Menubar is a horizontal bar holding several MenubarMenus; each menu’s label is its trigger text on the bar and its default slot holds the items. Clicking a trigger opens the menu, and while one is open moving the pointer onto another trigger switches to it. Items, checkbox items, radio items, groups, labels, separators and submenus are the same set as in DropdownMenu, only named with the Menubar prefix.

<script setup lang="ts">
  import { Menubar, MenubarItem, MenubarMenu } from '@hina-ui/vue'
</script>

<template>
  <Menubar label="Example menu">
    <MenubarMenu label="File">
      <MenubarItem>New</MenubarItem>
      <MenubarItem>Open</MenubarItem>
      <MenubarItem>Save</MenubarItem>
    </MenubarMenu>
    <MenubarMenu label="Edit">
      <MenubarItem>Undo</MenubarItem>
      <MenubarItem>Redo</MenubarItem>
    </MenubarMenu>
    <MenubarMenu label="Help">
      <MenubarItem>Documentation</MenubarItem>
      <MenubarItem>About</MenubarItem>
    </MenubarMenu>
  </Menubar>
</template>
vue

Examples

Items

The icon slot of an item holds a leading icon and trailing holds trailing content such as a shortcut; give irreversible actions such as delete tone="danger". MenubarLabel is a title that cannot be selected, and MenubarSeparator draws a line.

<script setup lang="ts">
  import { Copy, FilePlus, Trash2 } from '@lucide/vue'
  import {
    Kbd,
    Menubar,
    MenubarItem,
    MenubarLabel,
    MenubarMenu,
    MenubarSeparator,
  } from '@hina-ui/vue'
</script>

<template>
  <Menubar label="Item example">
    <MenubarMenu label="File">
      <MenubarLabel>This document</MenubarLabel>
      <MenubarItem>
        <template #icon><FilePlus /></template>
        New
        <template #trailing><Kbd>Ctrl N</Kbd></template>
      </MenubarItem>
      <MenubarItem>
        <template #icon><Copy /></template>
        Duplicate
      </MenubarItem>
      <MenubarSeparator />
      <MenubarItem tone="danger">
        <template #icon><Trash2 /></template>
        Delete
      </MenubarItem>
    </MenubarMenu>
  </Menubar>
</template>
vue

Checkbox and radio items

MenubarCheckboxItem is a toggle where several can be on at once and the menu stays open after toggling; wrap a set of mutually exclusive options in MenubarRadioGroup.

Word wrap on, line numbers off, font size medium

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Menubar,
    MenubarCheckboxItem,
    MenubarLabel,
    MenubarMenu,
    MenubarRadioGroup,
    MenubarRadioItem,
    MenubarSeparator,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const wrap = ref(true)
  const numbers = ref(false)
  const size = ref('md')
  const names: Record<string, string> = { sm: 'small', md: 'medium', lg: 'large' }
</script>

<template>
  <Stack gap="sm" align="start">
    <Menubar label="View settings">
      <MenubarMenu label="View">
        <MenubarCheckboxItem v-model:checked="wrap">Word wrap</MenubarCheckboxItem>
        <MenubarCheckboxItem v-model:checked="numbers">Line numbers</MenubarCheckboxItem>
        <MenubarSeparator />
        <MenubarLabel>Font size</MenubarLabel>
        <MenubarRadioGroup v-model="size">
          <MenubarRadioItem value="sm">Small</MenubarRadioItem>
          <MenubarRadioItem value="md">Medium</MenubarRadioItem>
          <MenubarRadioItem value="lg">Large</MenubarRadioItem>
        </MenubarRadioGroup>
      </MenubarMenu>
    </Menubar>
    <Text tone="muted" size="sm">
      Word wrap {{ wrap ? 'on' : 'off' }}, line numbers {{ numbers ? 'on' : 'off' }}, font size
      {{ names[size] }}
    </Text>
  </Stack>
</template>
vue

Controlled

v-model binds the value of the open menu, an empty string when none is open; a menu can be opened from outside.

Open now: none

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Menubar, MenubarItem, MenubarMenu, Stack, Text } from '@hina-ui/vue'

  const open = ref('')
</script>

<template>
  <Stack gap="md" align="start">
    <Inline gap="sm">
      <Button variant="outline" tone="neutral" @click="open = 'file'">Open "File"</Button>
      <Button variant="outline" tone="neutral" @click="open = 'help'">Open "Help"</Button>
    </Inline>
    <Menubar v-model="open" label="Controlled menu">
      <MenubarMenu label="File" value="file">
        <MenubarItem>New</MenubarItem>
        <MenubarItem>Open</MenubarItem>
      </MenubarMenu>
      <MenubarMenu label="Help" value="help">
        <MenubarItem>Documentation</MenubarItem>
        <MenubarItem>About</MenubarItem>
      </MenubarMenu>
    </Menubar>
    <Text tone="muted" size="sm">Open now: {{ open || 'none' }}</Text>
  </Stack>
</template>
vue

Disabled menus

disabled on a MenubarMenu disables its trigger; disabled on an item only makes that item unselectable.

<script setup lang="ts">
  import { Menubar, MenubarItem, MenubarMenu } from '@hina-ui/vue'
</script>

<template>
  <Menubar label="Disabled example">
    <MenubarMenu label="File">
      <MenubarItem>New</MenubarItem>
      <MenubarItem disabled>Save as</MenubarItem>
    </MenubarMenu>
    <MenubarMenu label="Debug" disabled>
      <MenubarItem>Start debugging</MenubarItem>
    </MenubarMenu>
  </Menubar>
</template>
vue

Behavior

  • Clicking a trigger opens its menu and clicking again closes it; while a menu is open, moving the pointer onto another trigger switches to that menu.
  • Left and right arrows move between triggers and switch menus while one is open; up and down move between items, Enter or Space picks one, and Esc closes the menu and returns focus to the trigger.
  • The page keeps scrolling and stays interactive while a menu is open, which is what lets the pointer move between triggers; picking an item or clicking outside closes it.

Accessibility

  • The root is role="menubar", named through label; triggers are menuitems with aria-haspopup, and the open panel is role="menu".
  • The triggers form a roving tab stop: Tab enters and leaves once, and the arrow keys move within.

API

Menubar

Prop
Type
Default
Description
label
string
—
Accessible name of the bar
loop
boolean
true
Whether the arrow keys wrap around at the ends
modelValue
string
—
value of the open menu, supports two-way binding
class
string
—
Classes appended to the root element

Items and other parts

MenubarItem, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarGroup, MenubarLabel, MenubarSeparator and MenubarSub take exactly the props, slots and events of their DropdownMenu counterparts; see DropdownMenu.