Skip to content

@vibe-labs/design-vue-menus

Vue 3 navigation menu components for the Vibe Design System. Vertical and horizontal menus with groups, flyouts, keyboard navigation, and Vue Router integration.

Installation

ts
import { VibeMenu, VibeMenuItem, VibeMenuGroup, VibeMenuDivider, VibeMenuFlyout } from "@vibe-labs/design-vue-menus";

Requires the CSS layer from @vibe-labs/design-components-menus.

Components

VibeMenu

Root menu container. Provides context (horizontal/compact/depth) to children via inject.

Usage

vue
<!-- Basic navigation -->
<VibeMenu nav aria-label="Main navigation">
  <VibeMenuItem href="/" label="Home" active />
  <VibeMenuItem href="/library" label="Library" />
  <VibeMenuItem href="/settings" label="Settings" />
</VibeMenu>

<!-- Horizontal -->
<VibeMenu nav horizontal aria-label="Top bar">
  <VibeMenuItem href="/" label="Home" />
  <VibeMenuItem href="/explore" label="Explore" />
</VibeMenu>

<!-- Compact (icons only) -->
<VibeMenu nav compact aria-label="Sidebar">
  <VibeMenuItem href="/">
    <template #icon><HomeIcon /></template>
  </VibeMenuItem>
</VibeMenu>

<!-- Plain list (no <nav> wrapper) -->
<VibeMenu>
  <VibeMenuItem label="Option A" />
  <VibeMenuItem label="Option B" />
</VibeMenu>

Props

PropTypeDefaultDescription
navbooleanfalseWrap in <nav> element
ariaLabelstringAccessible label
horizontalbooleanfalseHorizontal layout
compactbooleanfalseIcons-only mode
tag"ul" | "ol""ul"List element tag

Exposed Methods

MethodDescription
focusFirst()Focus the first menu item
focusLast()Focus the last menu item
elDirect ref to the list element

VibeMenuItem

Polymorphic menu item — renders as <button>, <a>, <router-link>, or custom component.

vue
<!-- Button (default) -->
<VibeMenuItem label="Action" @click="handleAction" />

<!-- Link -->
<VibeMenuItem href="/page" label="Page" />

<!-- Vue Router link -->
<VibeMenuItem :to="{ name: 'dashboard' }" label="Dashboard" />

<!-- External link -->
<VibeMenuItem href="https://example.com" target="_blank" label="External" />

<!-- Active state -->
<VibeMenuItem href="/current" label="Current Page" active />

<!-- With icon -->
<VibeMenuItem label="Settings">
  <template #icon><SettingsIcon /></template>
</VibeMenuItem>

<!-- With trail badge -->
<VibeMenuItem label="Notifications" trail="12" />

<!-- With trail slot -->
<VibeMenuItem label="Messages">
  <template #trail><VibeBadgeCount :count="5" /></template>
</VibeMenuItem>

<!-- Parent item with chevron -->
<VibeMenuItem label="More" chevron :expanded="isOpen" />

<!-- Disabled -->
<VibeMenuItem label="Unavailable" disabled />

<!-- Custom element -->
<VibeMenuItem :as="NuxtLink" to="/page" label="Nuxt Link" />

Props

PropTypeDefaultDescription
labelstringItem text (or use default slot)
activebooleanfalseCurrent page (sets aria-current="page")
disabledbooleanfalseDisabled state
hrefstringURL (renders as <a>)
tostring | objectVue Router destination (renders as <router-link>)
asstring | ComponentCustom render element
targetstringLink target
trailstringTrailing text/badge
chevronbooleanfalseShow expand chevron
expandedbooleanaria-expanded for parent items

Slots

SlotDescription
defaultLabel content
iconLeading icon
trailTrailing content
chevronCustom chevron icon
submenuNested content rendered after the item

VibeMenuGroup

Labelled group of items with optional collapse behaviour.

vue
<!-- Static group -->
<VibeMenuGroup label="Library">
  <VibeMenuItem label="Playlists" />
  <VibeMenuItem label="Albums" />
  <VibeMenuItem label="Artists" />
</VibeMenuGroup>

<!-- Collapsible -->
<VibeMenuGroup label="Advanced" collapsible>
  <VibeMenuItem label="Developer" />
  <VibeMenuItem label="API Keys" />
</VibeMenuGroup>

<!-- Controlled collapse -->
<VibeMenuGroup label="Section" collapsible v-model:expanded="isOpen">
  <VibeMenuItem label="Child" />
</VibeMenuGroup>

<!-- Default collapsed -->
<VibeMenuGroup label="Hidden" collapsible :default-expanded="false">
  <VibeMenuItem label="Revealed on expand" />
</VibeMenuGroup>

Props

PropTypeDefaultDescription
labelstringGroup heading
collapsiblebooleanfalseEnable expand/collapse
expandedbooleanControlled expanded state (v-model:expanded)
defaultExpandedbooleantrueInitial state when uncontrolled
idstringautoID for aria-labelledby

VibeMenuFlyout

Nested submenu that appears on hover or click.

vue
<!-- Hover flyout (default) -->
<VibeMenuFlyout label="More Options">
  <VibeMenuItem label="Export" />
  <VibeMenuItem label="Share" />
</VibeMenuFlyout>

<!-- Click trigger -->
<VibeMenuFlyout label="Actions" trigger="click">
  <VibeMenuItem label="Edit" />
  <VibeMenuItem label="Delete" />
</VibeMenuFlyout>

<!-- Custom trigger -->
<VibeMenuFlyout>
  <template #trigger="{ open, toggle, attrs }">
    <button v-bind="attrs" @click="toggle">
      Custom trigger {{ open ? '▼' : '▶' }}
    </button>
  </template>
  <VibeMenuItem label="Sub-option" />
</VibeMenuFlyout>

<!-- Alignment -->
<VibeMenuFlyout label="Options" align="left">
  <VibeMenuItem label="Left-aligned panel" />
</VibeMenuFlyout>

<!-- Controlled -->
<VibeMenuFlyout v-model:open="flyoutOpen" label="Controlled">
  <VibeMenuItem label="Option" />
</VibeMenuFlyout>

Props

PropTypeDefaultDescription
alignMenuFlyoutAlignment"right"Panel alignment
openbooleanControlled open state (v-model:open)
trigger"hover" | "click""hover"How to open
labelstringBuilt-in trigger label
disabledbooleanDisable the built-in trigger

Trigger Slot

PropertyTypeDescription
openbooleanCurrent open state
toggle() => voidToggle open/close
attrsobjectARIA attrs to spread onto trigger element

VibeMenuDivider

Visual separator between items.

vue
<VibeMenuItem label="Edit" />
<VibeMenuDivider />
<VibeMenuItem label="Delete" />

Composable

useMenuKeyboard(options)

Arrow-key navigation for menu containers. Horizontal-aware (swaps up/down for left/right). Supports wrap-around and Home/End.

ts
import { useMenuKeyboard } from "@vibe-labs/design-vue-menus";

const menuEl = ref<HTMLElement | null>(null);
const { focusFirst, focusLast } = useMenuKeyboard({
  containerRef: menuEl,
  horizontal: false,
  wrap: true, // default
});

Keyboard Navigation

KeyVerticalHorizontal
ArrowDown / ArrowRightNext itemNext item
ArrowUp / ArrowLeftPrevious itemPrevious item
HomeFirst itemFirst item
EndLast itemLast item
EscapeClose flyoutClose flyout

Nesting

Menus auto-track nesting depth via provide/inject. Each VibeMenu increments the depth counter, so nested menus know their level (0 = root, 1 = first nested, etc.).

Dependencies

PackagePurpose
@vibe-labs/design-components-menusCSS tokens + generated styles

Optional peer: vue-router (externalized, only needed if using to prop on items).

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.