Appearance
@vibe-labs/design-components-menus
Navigation menu component tokens, styles, and TypeScript types for the Vibe Design System. For persistent, structural navigation (sidebars, top bars, multi-level). Distinct from dropdowns (triggered popups).
Installation
bash
npm install @vibe-labs/design-components-menuscss
@import "@vibe-labs/design-components-menus";ts
import { MenuFlyoutAlignments } from "@vibe-labs/design-components-menus/types";
import type { MenuStyleProps, MenuItemStyleProps, MenuFlyoutStyleProps } from "@vibe-labs/design-components-menus/types";Contents
Tokens
CSS custom properties defined in @layer vibe.tokens (file: menu.css).
Item
| Token | Default | Description |
|---|---|---|
--menu-item-height | 2.25rem | Minimum item height |
--menu-item-px | --space-3 | Horizontal padding |
--menu-item-radius | --radius-md | Border radius |
--menu-item-font-size | --text-sm | Font size |
--menu-item-font-weight | 400 | Default weight |
--menu-item-color | --text-secondary | Text color |
--menu-item-hover-bg | --surface-hover-overlay | Hover background |
--menu-item-active-bg | --surface-active-overlay | Active/current background |
--menu-item-active-color | --text-primary | Active text color |
--menu-item-active-font-weight | --font-semibold | Active weight |
--menu-item-active-indicator | --color-accent | Active indicator bar color |
Icons
| Token | Default |
|---|---|
--menu-icon-size | 1.125rem |
--menu-icon-color | --text-muted |
--menu-icon-active-color | --color-accent |
Groups & Dividers
| Token | Default |
|---|---|
--menu-group-label-font-size | --text-xs |
--menu-group-label-color | --text-muted |
--menu-group-label-font-weight | --font-semibold |
--menu-divider-color | --border-subtle |
Nesting
| Token | Default | Description |
|---|---|---|
--menu-indent | --space-4 | Nested menu left padding |
--menu-nested-indicator-width | 1px | Vertical tree line width |
--menu-nested-indicator-color | --border-subtle | Tree line color |
Submenu (Flyout)
| Token | Default |
|---|---|
--menu-submenu-bg | --surface-elevated |
--menu-submenu-border-color | --border-subtle |
--menu-submenu-radius | --radius-lg |
--menu-submenu-shadow | --shadow-lg |
--menu-submenu-z | --z-dropdown |
--menu-collapse-speed | --duration-normal |
Generated Styles
Component classes generated into @layer vibe.components (file: menu.g.css).
Menu Container
.menu— vertical flex column, resets list stylesdata-horizontal— switches to row layout
Menu Item
- Full-width interactive row with hover/focus/disabled states
aria-currentsets active background, color, weight, and indicator bar- Vertical layout: left-side accent bar indicator (3px, 50% height)
- Horizontal layout: bottom bar indicator (2px, 50% width)
- Inset focus ring via
box-shadow
Item Slots
| Class | Description |
|---|---|
.menu-icon | Leading icon (accent-colored when active) |
.menu-label | Flex-grow text with ellipsis overflow |
.menu-trail | Trailing badge/count text |
.menu-chevron | Rotates 90° when aria-expanded="true" |
Groups & Dividers
| Class | Description |
|---|---|
.menu-group | Flex column wrapper |
.menu-group-label | Uppercase section header |
.menu-divider | Horizontal separator |
Nested Menus
- Indented with vertical tree-line indicator via
::before .menu-collapse— smoothgrid-template-rowsexpand/collapse animationdata-expandedon.menu-collapse— opens the collapsed section
Flyout Submenus
- Absolutely positioned popup with fade-in animation
- Alignment (
data-alignon.menu-flyout):right(default) ·bottom data-openon.menu-flyout— shows the flyout
Compact Mode
data-compact on .menu — hides labels, trails, chevrons, and group labels; centers items for icon-only sidebar navigation; hides nested tree-line indicators.
TypeScript Types
ts
MenuFlyoutAlignments // ["right", "bottom"]
type MenuFlyoutAlignment
interface MenuStyleProps { horizontal?: boolean; compact?: boolean }
interface MenuItemStyleProps { disabled?: boolean }
interface MenuIconStyleProps {}
interface MenuLabelStyleProps {}
interface MenuTrailStyleProps {}
interface MenuChevronStyleProps {}
interface MenuGroupStyleProps {}
interface MenuGroupLabelStyleProps {}
interface MenuDividerStyleProps {}
interface MenuCollapseStyleProps { expanded?: boolean }
interface MenuFlyoutStyleProps { align?: MenuFlyoutAlignment; open?: boolean }Dist Structure
| File | Description |
|---|---|
index.css | Barrel — imports tokens + generated styles |
menu.css | Token definitions |
menu.g.css | Generated component styles |
index.js / index.d.ts | TypeScript types + runtime constants |
Dependencies
Requires tokens from @vibe-labs/design (surfaces, borders, spacing, typography, colors, transitions, elevation, z-index).
Build
bash
npm run buildUsage Guide
Import
css
@import "@vibe-labs/design-components-menus";ts
import type { MenuStyleProps, MenuItemStyleProps, MenuFlyoutStyleProps } from "@vibe-labs/design-components-menus/types";Variants
Container modifiers
data-horizontalon.menu— switches to row layout (top navigation bar)data-compacton.menu— icon-only sidebar (hides labels, trails, chevrons)
Active item
aria-current on .menu-item — applies active background, text color, weight, and accent indicator bar.
Chevron expanded
aria-expanded="true" on .menu-chevron — rotates the chevron 90°.
Collapsed section
data-expanded on .menu-collapse — opens the animated expand/collapse section.
Flyout alignment
data-align on .menu-flyout: right (default) · bottom
data-open on .menu-flyout — shows the flyout with fade-in animation.
Disabled item
data-disabled on .menu-item — reduces opacity, disables pointer events.
Examples
Vertical sidebar navigation
html
<!-- Standard vertical menu for a sidebar -->
<nav>
<ul class="menu" role="list">
<li>
<a class="menu-item" href="/dashboard" aria-current="page">
<span class="menu-icon"><svg><!-- icon --></svg></span>
<span class="menu-label">Dashboard</span>
</a>
</li>
<li>
<a class="menu-item" href="/projects">
<span class="menu-icon"><svg><!-- icon --></svg></span>
<span class="menu-label">Projects</span>
<span class="menu-trail">12</span>
</a>
</li>
<li>
<a class="menu-item" href="/settings">
<span class="menu-icon"><svg><!-- icon --></svg></span>
<span class="menu-label">Settings</span>
</a>
</li>
</ul>
</nav>Horizontal top navigation bar
html
<!-- Horizontal menu — indicator bar appears at the bottom of each item -->
<nav>
<ul class="menu" data-horizontal role="list">
<li><a class="menu-item" href="/" aria-current="page"><span class="menu-label">Home</span></a></li>
<li><a class="menu-item" href="/docs"><span class="menu-label">Docs</span></a></li>
<li><a class="menu-item" href="/blog"><span class="menu-label">Blog</span></a></li>
</ul>
</nav>Compact (icon-only) sidebar
html
<!-- Compact mode — only icons visible, labels hidden -->
<nav>
<ul class="menu" data-compact role="list">
<li>
<a class="menu-item" href="/dashboard" aria-current="page" aria-label="Dashboard">
<span class="menu-icon"><svg><!-- icon --></svg></span>
</a>
</li>
<li>
<a class="menu-item" href="/projects" aria-label="Projects">
<span class="menu-icon"><svg><!-- icon --></svg></span>
</a>
</li>
</ul>
</nav>Grouped menu with dividers
html
<nav>
<ul class="menu">
<li class="menu-group">
<div class="menu-group-label">Main</div>
<ul>
<li><a class="menu-item" href="/dashboard" aria-current="page"><span class="menu-label">Dashboard</span></a></li>
<li><a class="menu-item" href="/analytics"><span class="menu-label">Analytics</span></a></li>
</ul>
</li>
<li class="menu-divider"></li>
<li class="menu-group">
<div class="menu-group-label">Settings</div>
<ul>
<li><a class="menu-item" href="/profile"><span class="menu-label">Profile</span></a></li>
</ul>
</li>
</ul>
</nav>Collapsible nested section
html
<!-- Toggle data-expanded to expand/collapse the child menu -->
<ul class="menu">
<li>
<button class="menu-item" aria-expanded="true">
<span class="menu-icon"><svg><!-- icon --></svg></span>
<span class="menu-label">Components</span>
<span class="menu-chevron" aria-expanded="true"><svg><!-- chevron --></svg></span>
</button>
<div class="menu-collapse" data-expanded>
<ul class="menu">
<li><a class="menu-item" href="/components/buttons"><span class="menu-label">Buttons</span></a></li>
<li><a class="menu-item" href="/components/inputs"><span class="menu-label">Inputs</span></a></li>
</ul>
</div>
</li>
</ul>Flyout submenu
html
<!-- Flyout opens to the right of the parent item by default -->
<ul class="menu">
<li style="position: relative">
<button class="menu-item" aria-haspopup="true">
<span class="menu-label">More</span>
<span class="menu-chevron"><svg><!-- › --></svg></span>
</button>
<div class="menu-flyout" data-align="right" data-open>
<a class="menu-item" href="/changelog"><span class="menu-label">Changelog</span></a>
<a class="menu-item" href="/roadmap"><span class="menu-label">Roadmap</span></a>
</div>
</li>
</ul>With Vue
Use @vibe-labs/design-vue-menus for <SbMenu>, <SbMenuItem>, <SbMenuGroup>, <SbMenuCollapse>, and <SbMenuFlyout> which manage aria-current, expand/collapse state, and flyout positioning automatically.