The primary app navigation, dark in both themes. The active section's submenu stays open in place, other sections open as flyouts, and the rail turns every submenu into a flyout. Data-driven from one nav config that also feeds Breadcrumb, Page header and ⌘K — or composed by hand with identical DOM.
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarNav, SidebarCollapseButton, AppSwitcher, type NavNode } from "@vamos-ui/react";
const nav: NavNode[] = [
{ id: "dashboard", label: "Dashboard", href: "/site", icon: LayoutDashboard },
{ type: "group", id: "content", label: "Content", items: [
{ id: "posts", label: "Posts", href: "/site/posts", icon: FileText, children: [
{ id: "posts.all", label: "All posts", href: "/site/posts" },
{ id: "posts.categories", label: "Categories", href: "/site/posts/categories" },
] },
] },
];
// Link + currentPath come from VamosProvider (router-agnostic).
<Sidebar>
<SidebarHeader><AppSwitcher apps={apps} activeApp="site" /></SidebarHeader>
<SidebarContent><SidebarNav items={nav} badges={{ comments: 4 }} /></SidebarContent>
<SidebarFooter><SidebarCollapseButton /></SidebarFooter>
</Sidebar>Keyboard: Tab moves between items (this is site navigation, not role=menu). → (← in RTL) opens a flyout and focuses its first link; ↑↓ move inside; Esc or ← closes and returns focus.
Touch: the first tap on a section with a flyout opens it, the second follows the link.
Flyouts are portalled, so the scrolling menu can't clip them, and never blur (an almost opaque --nav-popover instead).
Examples
Composed by hand
SidebarGroup, SidebarItem and SidebarSubItem are the parts SidebarNav renders. Without a nav config an item is active when its href (or a child's) matches the current path; pass active to override.
Rail
Collapsed, every submenu opens as a flyout headed by the section name, leaves get a tooltip and badges become dots. In the App shell mod+\ toggles it (never mod+b — that is Bold in editors) and the choice is stored in the ui-sidebar cookie.
Three levels
Level 3 is a collapsible inline and a heading plus list inside a flyout. Items without href only open their children. A string disabled shows a reason badge.
Live badges
The badges map is separate from the config. Counts over 99 show 99+, the rail shows a dot, and the text joins the link's name: “Comments, 4 awaiting moderation”.
Role-based menu
filterNav runs before render, so hidden items aren't in the DOM. It is UI only: enforce every permission on the server.
Surface variant (client portal)
variant="surface" takes the content background — for client portals and websites. With one app the switcher is a static title.
API
Sidebar
| Prop | Type | Default |
|---|---|---|
variantDark navigation in both themes, or the content surface. | "nav" | "surface" | "nav" |
collapsed / defaultCollapsed / onCollapsedChangeRail mode. | boolean | false |
collapsibleAllow the rail. | boolean | true |
modemobile: always expanded, accordion levels (App shell's sheet). | "desktop" | "mobile" | "desktop" |
shortcutToggle shortcut, e.g. "mod+\". App shell turns it on. | string | false | false |
onNavigateCalled after a link is followed. | () => void | — |
labelsOverride built-in strings (sidebar.*). | Partial<SidebarLabels> | — |
SidebarNav
| Prop | Type | Default |
|---|---|---|
itemsItems, groups ({ type: "group" }) and separators. | NavNode[] | — |
badgesLive counts by item id: { comments: 4, forms: { count: 12, tone: "danger" } }. | Record<string, BadgeSpec> | — |
featuresFeature flags for items with feature. | Record<string, boolean> | — |
trailActive trail override; default resolveActive(items, currentPath). | string[] | — |
aria-labelWrap in its own <nav> (a second menu in the footer). | string | — |
NavItem
| Prop | Type | Default |
|---|---|---|
idStable key for badges, the active trail and ⌘K. | string | — |
label / href / iconNo href: a parent that only opens its children. | string / string / LucideIcon | — |
childrenUp to 3 levels. | NavItem[] | — |
matchExtra active patterns, "/posts/*" = any child path. | string[] | (path) => boolean | — |
permissionHidden unless can() — UI only. | string | string[] | — |
featureHidden unless features[feature]. | string | — |
externalNew tab + ↗ (detected from absolute hrefs). | boolean | — |
keywordsExtra ⌘K search terms. | string[] | — |
badgeStatic badge: number, string or { count, tone, dot, label }. | BadgeSpec | — |
disabledA string is shown as the reason ("Coming soon"). | boolean | string | — |
SidebarItem / SidebarSubItem
| Prop | Type | Default |
|---|---|---|
idMatches badges and the trail in SidebarNav. | string | — |
label / href / iconicon on SidebarItem only. | string | — |
badge / disabled / externalAs in NavItem. | BadgeSpec / boolean | string / boolean | — |
activeForce the active state. | boolean | — |
childrenSubmenu (inline when active, flyout otherwise). | SidebarSubItem[] | — |
SidebarGroup · SidebarHeader · SidebarContent · SidebarFooter · SidebarSeparator · SidebarCollapseButton · SidebarSkeleton
| Prop | Type | Default |
|---|---|---|
labelSidebarGroup heading; a hairline in the rail. | ReactNode | — |
aria-labelSidebarContent is the <nav>. | string | "Main navigation" |
rowsSidebarSkeleton rows while permissions load. | number | 6 |
AppSwitcher
| Prop | Type | Default |
|---|---|---|
appsEvery app the user can open. One app → static title. | { id, name, description?, icon?, href }[] | — |
activeAppCurrent app id. | string | — |
onSelectSwitch in place instead of following href. | (id) => void | — |
workspaces / activeWorkspace / onWorkspaceChangeTenant switcher in the same panel. | … | — |
footerLinks under the list (“Manage apps & modules…”). | ReactNode | — |
Helpers (lib/nav)
| Prop | Type | Default |
|---|---|---|
filterNav(nodes, { can, features })Drops hidden items, empty groups and orphan separators. | NavNode[] | — |
resolveActive(nodes, path)Longest-prefix match; / matches only exactly. | { trail, leafId } | — |
flattenNav(nodes)Navigable items with label trails, for ⌘K and Breadcrumb. | FlatNavItem[] | — |
parseSidebarCookie(value)Read the ui-sidebar cookie on the server. | boolean | undefined | — |