The one shell every app runs on — CMS, CRM, HR, PRM — with a different app definition. Sidebar, top bar, content, skip link, mobile sheet nav, a ⌘K palette with “Go to” commands derived from every app's nav, toasts and confirm. Router-agnostic.
Northwind
Dashboard
Visitors · 30 days
28,412+8.4%
Published posts
144
Form submissions
312
Recent posts
/site- A field guide to GDPR consent bannersPublishedNov 9
- Writing alt text that actually helpsScheduledNov 12
- Local SEO checklist for 2026DraftNov 6
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
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { AppShellProvider, AppShell, NotificationCenter, type AppDefinition } from "@vamos-ui/react";
const site: AppDefinition = { id: "site", name: "Site", href: "/site", icon: Globe, nav: siteNav, links: [{ id: "view", label: "View site", href: "https://example.com" }] };
const crm: AppDefinition = { id: "crm", name: "CRM", href: "/crm", icon: Handshake, nav: crmNav, accent: "ocean" };
export function Shell({ defaultCollapsed, children }) {
const router = useRouter();
return (
<AppShellProvider apps={[site, crm]} sharedNav={shared} linkComponent={Link} currentPath={usePathname()} navigate={router.push} defaultCollapsed={defaultCollapsed}>
<AppShell user={{ name: "Ada Lovelace", email: "ada@northwind.io", items: account }} notifications={<NotificationCenter adapter={inbox} />}>
{children}
</AppShell>
</AppShellProvider>
);
}Landmarks: banner (top bar), navigation (sidebar), main (skip-link target, focused after route changes). The skip link is the first focusable element.
App definitions contain components and functions, so keep them in a client module; compute defaultCollapsed and activeApp in the server layout to avoid a hydration flash.
Glass: the shell owns the two permanent blurs (sidebar, top bar); flyouts and nested layers never blur.
Examples
Multi-app
Switching apps swaps the nav, commands, “New ▾” menu, app links and accent. Top bar, user menu, notifications and ⌘K stay. Apps a user can't open (permission) never appear.
CRM
Customers & sales
Open deals
37
Pipeline value
$412,800
Won this month
$68,450+12%
Deals closing soon
/crm/pipeline- Fjord Freight · Fleet trackingProposal sentDec 15
- Harbor & Pine Legal · RetainerNegotiationDec 18
- Brightline Studio · Site rebuildDiscoveryJan 9
Workspaces
A workspace/tenant switcher shares the app switcher's panel for multi-company setups.
Defne Spa
Posts
Content for Defne Spa.
Visitors · 30 days
28,412+8.4%
Published posts
144
Form submissions
312
Recent posts
/site/posts- A field guide to GDPR consent bannersPublishedNov 9
- Writing alt text that actually helpsScheduledNov 12
- Local SEO checklist for 2026DraftNov 6
Mobile
The shell responds to its own width: below 768px the sidebar moves into a start-side sheet with accordion levels; 768–1279px defaults to the rail when the user has no preference.
Categories
Visitors · 30 days
28,412+8.4%
Published posts
144
Form submissions
312
Recent posts
/site/posts/categories- A field guide to GDPR consent bannersPublishedNov 9
- Writing alt text that actually helpsScheduledNov 12
- Local SEO checklist for 2026DraftNov 6
Next.js router
Pass Link, usePathname() and router.push. Read the ui-sidebar cookie in the server layout with parseSidebarCookie and pass defaultCollapsed to avoid a flash. These links navigate this site.
Routing
The shell reads the route from Next and navigates through it — nothing else to wire.
- linkComponent
next/linkSidebar, breadcrumb and app switcher links- currentPath
/docs/components/app-shell/Active item and automatic breadcrumb- navigate
router.pushCommand palette and keyboard shortcuts
API
AppShellProvider
| Prop | Type | Default |
|---|---|---|
appsEvery app on the shell. Filtered by permission. | AppDefinition[] | — |
activeAppDefault: the app whose base/href is the longest prefix of currentPath, else the last one used. | string | — |
sharedNavBottom menu shared by every app (users, settings). | NavNode[] | — |
commandsCommands available in every app. | CommandAction[] | — |
linkComponent / currentPath / navigate / canRouter and permission pieces; omit when a VamosProvider above sets them. | VamosProvider bridge | — |
featuresFeature flags for nav items. | Record<string, boolean> | — |
defaultCollapsedRail preference from the ui-sidebar cookie; undefined = follow the width. | boolean | — |
onCollapsedChangePersist the preference elsewhere. | (v: boolean) => void | writes the cookie |
workspaces / activeWorkspace / onWorkspaceChangeTenant switcher. | … | — |
shortcutsmod+k (palette) and mod+\ (rail). Off for embedded shells. | boolean | true |
locale / messages / dirPassed to the LocaleProvider. | … | — |
AppShell
| Prop | Type | Default |
|---|---|---|
userUser menu in the default top bar. | UserMenuProps | — |
notifications<NotificationCenter adapter={…} /> slot. | ReactNode | — |
actionsExtra top-bar actions (theme, help). | ReactNode | — |
badgesLive sidebar badges by nav id. | Record<string, BadgeSpec> | — |
bannerSlot above the top bar (offline, trial). | ReactNode | — |
sidebar / topbarReplace or remove the defaults. | ReactNode | false | — |
sidebarFooter / appSwitcherFooterExtra sidebar content. | ReactNode | — |
layoutPortal-style top navigation. | "sidebar" | "topnav" | "sidebar" |
loadingSidebar skeleton while permissions load. | boolean | false |
announceRouteChangesFocus main and announce the page title after navigation. | boolean | true |
heightfill: fill the parent (embedded previews). | "screen" | "fill" | "screen" |
globalsMount the ⌘K palette, Toaster, ConfirmProvider and TooltipProvider. | boolean | true |
AppDefinition
| Prop | Type | Default |
|---|---|---|
id / name / description / iconShown in the app switcher and as the ⌘K app badge. | string / ReactNode | — |
href / baseApp home and its path prefix. An absolute href does a full navigation (multi-zone). | string | — |
accentdata-accent while active (CRM = ocean). | string | — |
navThe app's sidebar; also ⌘K “Go to”. | NavNode[] | — |
commands / sources⌘K commands (active app) and record search (always). | CommandAction[] / CommandSource[] | — |
newMenuTop bar “New ▾”. | MenuEntry[] | — |
linksTop-bar links for this app only — “View site” in the Site app. | { id, label, href, icon?, external? }[] | — |
permissionHide the app entirely. | string | — |
useAppShell()
| Prop | Type | Default |
|---|---|---|
activeApp / apps / nav / sharedNavCurrent state (nav already filtered). | … | — |
collapsed / setCollapsedRail. | boolean / (v) => void | — |
openMobileNav / closeMobileNav / mobileNavOpenMobile sheet. | … | — |
trailActive nav trail (Breadcrumb, Page header). | string[] | — |
switchApp(id)Navigate to another app. | void | — |