Beta, expect API change, and bugs. Hit one? Tell us.

Skip to content

Menubar

A row of application menus.

<script setup lang="ts">
import { Menu } from '@shardsui/vue/menu'
import { Menubar } from '@shardsui/vue/menubar'

const menus = [
  {
    label: 'File',
    groups: [
      ['New file', 'Import', { label: 'Export as', options: ['PDF', 'CSV', 'PNG'] }],
      ['Refresh']
    ]
  },
  {
    label: 'Edit',
    groups: [['Undo', 'Redo']]
  }
]

function submenuOffset({ side }: { side: string }) {
  return side === 'top' || side === 'bottom' ? 4 : -4
}
</script>

<template>
  <Menubar class="flex items-center">
    <Menu.Root v-for="menu in menus" :key="menu.label">
      <Menu.Trigger
        class="h-8 rounded-sm px-3 text-sm font-normal text-gray-600 select-none focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-100"
        >{{ menu.label }}</Menu.Trigger
      >
      <Menu.Portal>
        <Menu.Positioner class="outline-hidden" :side-offset="6">
          <Menu.Popup
            class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-instant:transition-none data-starting-style:scale-95 data-starting-style:opacity-0"
          >
            <template v-for="(group, groupIndex) in menu.groups" :key="groupIndex">
              <Menu.Separator v-if="groupIndex > 0" class="m-1 h-px bg-gray-200" />
              <template
                v-for="entry in group"
                :key="typeof entry === 'string' ? entry : entry.label"
              >
                <Menu.Item
                  v-if="typeof entry === 'string'"
                  class="flex px-4 py-2 text-sm/4 font-normal outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
                  >{{ entry }}</Menu.Item
                >
                <Menu.SubmenuRoot v-else>
                  <Menu.SubmenuTrigger
                    class="flex items-center justify-between gap-4 py-2 pr-2 pl-4 text-sm/4 font-normal outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900 data-popup-open:relative data-popup-open:z-0 data-popup-open:before:absolute data-popup-open:before:inset-x-1 data-popup-open:before:inset-y-0 data-popup-open:before:z-[-1] data-popup-open:before:rounded-xs data-popup-open:before:bg-gray-100 data-highlighted:data-popup-open:before:bg-gray-900"
                  >
                    {{ entry.label }}
                    <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path
                        d="M9.5 18.25L15.75 12L9.5 5.75"
                        stroke="currentColor"
                        stroke-width="1.5"
                        stroke-linecap="round"
                        stroke-linejoin="round"
                      />
                    </svg>
                  </Menu.SubmenuTrigger>
                  <Menu.Portal>
                    <Menu.Positioner
                      class="outline-hidden"
                      :side-offset="submenuOffset"
                      :align-offset="submenuOffset"
                    >
                      <Menu.Popup
                        class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
                      >
                        <Menu.Item
                          v-for="option in entry.options"
                          :key="option"
                          class="flex px-4 py-2 text-sm/4 font-normal outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
                          >{{ option }}</Menu.Item
                        >
                      </Menu.Popup>
                    </Menu.Positioner>
                  </Menu.Portal>
                </Menu.SubmenuRoot>
              </template>
            </template>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>

    <Menu.Root disabled>
      <Menu.Trigger
        class="h-8 rounded-sm px-3 text-sm font-normal text-gray-600 select-none focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-disabled:opacity-50 data-popup-open:bg-gray-100"
        >Account</Menu.Trigger
      >
    </Menu.Root>
  </Menubar>
</template>

Anatomy

One <Menu.Root> per top-level menu:

<script setup>
import { Menubar } from '@shardsui/vue/menubar'
import { Menu } from '@shardsui/vue/menu'
</script>

<template>
  <Menubar>
    <Menu.Root>
      <Menu.Trigger />
      <Menu.Portal>
        <Menu.Backdrop />
        <Menu.Positioner>
          <Menu.Popup>
            <Menu.Arrow />
            <Menu.Item />
            <Menu.LinkItem />
            <Menu.Separator />

            <Menu.SubmenuRoot>
              <Menu.SubmenuTrigger />
            </Menu.SubmenuRoot>

            <Menu.Group>
              <Menu.GroupLabel />
            </Menu.Group>

            <Menu.RadioGroup>
              <Menu.RadioItem>
                <Menu.RadioItemIndicator />
              </Menu.RadioItem>
            </Menu.RadioGroup>

            <Menu.CheckboxItem>
              <Menu.CheckboxItemIndicator />
            </Menu.CheckboxItem>

            <Menu.Viewport />
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  </Menubar>
</template>

Each menu is a full Menu — its parts (Trigger, Positioner, Popup, Item, Group, RadioGroup, CheckboxItem, Viewport, etc.) share the same props and data attributes documented there.

API reference

PropTypeDefault
AttributeDescription
data-modalPresent when modal is true.
data-orientationIndicates the orientation of the menubar.
data-has-submenu-openPresent when any submenu within the menubar is open.