Appearance
@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
| Prop | Type | Default | Description |
|---|---|---|---|
nav | boolean | false | Wrap in <nav> element |
ariaLabel | string | — | Accessible label |
horizontal | boolean | false | Horizontal layout |
compact | boolean | false | Icons-only mode |
tag | "ul" | "ol" | "ul" | List element tag |
Exposed Methods
| Method | Description |
|---|---|
focusFirst() | Focus the first menu item |
focusLast() | Focus the last menu item |
el | Direct 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
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Item text (or use default slot) |
active | boolean | false | Current page (sets aria-current="page") |
disabled | boolean | false | Disabled state |
href | string | — | URL (renders as <a>) |
to | string | object | — | Vue Router destination (renders as <router-link>) |
as | string | Component | — | Custom render element |
target | string | — | Link target |
trail | string | — | Trailing text/badge |
chevron | boolean | false | Show expand chevron |
expanded | boolean | — | aria-expanded for parent items |
Slots
| Slot | Description |
|---|---|
default | Label content |
icon | Leading icon |
trail | Trailing content |
chevron | Custom chevron icon |
submenu | Nested 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
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Group heading |
collapsible | boolean | false | Enable expand/collapse |
expanded | boolean | — | Controlled expanded state (v-model:expanded) |
defaultExpanded | boolean | true | Initial state when uncontrolled |
id | string | auto | ID 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
| Prop | Type | Default | Description |
|---|---|---|---|
align | MenuFlyoutAlignment | "right" | Panel alignment |
open | boolean | — | Controlled open state (v-model:open) |
trigger | "hover" | "click" | "hover" | How to open |
label | string | — | Built-in trigger label |
disabled | boolean | — | Disable the built-in trigger |
Trigger Slot
| Property | Type | Description |
|---|---|---|
open | boolean | Current open state |
toggle | () => void | Toggle open/close |
attrs | object | ARIA 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
| Key | Vertical | Horizontal |
|---|---|---|
| ArrowDown / ArrowRight | Next item | Next item |
| ArrowUp / ArrowLeft | Previous item | Previous item |
| Home | First item | First item |
| End | Last item | Last item |
| Escape | Close flyout | Close 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
| Package | Purpose |
|---|---|
@vibe-labs/design-components-menus | CSS tokens + generated styles |
Optional peer: vue-router (externalized, only needed if using to prop on items).
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.