Sidebar
The dashboard's side navigation: the pages in a menu (the app itself keeps the studio and signing out in its header's StudioMenu). shadcn's sidebar as vendored, unrestyled — porcelain beside the white page, the current page on a faint ink wash.
Playground
import { … } from "@/components/shared/sidebar";
<SidebarProvider>
<Sidebar collapsible="offcanvas">
<SidebarHeader className="px-4 py-4">
<p className="text-lg font-medium tracking-tight">Okami</p>
<p className="text-muted-foreground truncate text-sm">Studio Lumen</p>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton render={<Link href="/" />} isActive>
<Glyph icon={Home01Icon} />
<span>Home</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton render={<Link href="/inbox" />}>
<Glyph icon={InboxIcon} />
<span>Inbox</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton render={<Link href="/services" />}>
<Glyph icon={TagIcon} />
<span>Services</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton render={<Link href="/clients" />}>
<Glyph icon={UsersIcon} />
<span>Clients</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>Held in a frame here with collapsible="none"; the dashboard's sidebar is fixed to the viewport's edge (see Collapsing).
In the dashboard
The labelled pages, opened from okami's logo in a popover on a phone where the shell has no tab bar; the dashboard has one, the TabBar under its composer (from lg the same pages are the NavRail). The nav marks the current page from the path — an exact match for Home at the root, a prefix match for the rest, so a service's own page still lights Services.
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/shared/sidebar";
// app/[locale]/app/(dashboard)/layout.tsx — AppShell puts it in the logo's menu
<AppShell
sidebar={
<SidebarContent>
<DashboardNav locale={locale} />
</SidebarContent>
}
rail={<DashboardRail locale={locale} />}
…
/>
// components/dashboard/nav.tsx
<SidebarGroup>
<SidebarMenu>
{NAV_PAGES.map((page) => (
<SidebarMenuItem key={page.key}>
<SidebarMenuButton
render={<Link href={`/${locale}${page.path}`} />}
isActive={page.key === current}
>
<page.icon aria-hidden />
<span>{t(page.key)}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>Menu button states
At rest it's plain ink; hover and press lay a faint ink wash; isActive holds the wash and sets the label medium; disabled dims it and ignores the pointer.
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/shared/sidebar";
<SidebarMenuButton>At rest</SidebarMenuButton>
<SidebarMenuButton isActive>Current</SidebarMenuButton>
<SidebarMenuButton disabled>Disabled</SidebarMenuButton>Collapsing
From md up the sidebar is fixed to the left edge and offcanvas slides it away when closed; below md it becomes a sheet. SidebarTrigger toggles either, and so does ⌘B / Ctrl B. The open state is kept in the sidebar_state cookie. It moves as a panel reveal: out on duration-medium, back on duration-slow, softly blurred, the page following in step; the sheet and its dimmed page do the same.
<SidebarInset>
<header className="flex h-14 items-center gap-2 border-b px-4 md:hidden">
<SidebarTrigger />
<span className="text-sm font-medium">{venue.name}</span>
</header>
{children}
</SidebarInset>API reference
SidebarProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultOpen | boolean | true | Open on first render, uncontrolled. |
| open | boolean | — | Controls the open state, with onOpenChange. |
| onOpenChange | (open: boolean) => void | — | Called when the trigger, rail or ⌘B toggles it. |
Sidebar
| Prop | Type | Default | Description |
|---|---|---|---|
| collapsible | "offcanvas" | "icon" | "none" | "offcanvas" | Slide away, shrink to icons, or sit in place. The app's menu sits, in the logo's popover. |
| side | "left" | "right" | "left" | Which edge it's fixed to. |
| variant | "sidebar" | "floating" | "inset" | "sidebar" | Flush with a hairline edge, a floating panel, or inset beside a rounded page. |
SidebarMenuButton
| Prop | Type | Default | Description |
|---|---|---|---|
| render | ReactElement | <button> | The element to render as — a link for navigation: render={<Link href=… />}. |
| isActive | boolean | false | The current page: holds the wash, medium label. |
| size | "default" | "sm" | "lg" | "default" | 32, 28 or 48px tall. |
| variant | "default" | "outline" | "default" | Outline adds a hairline edge on a white fill. |
| tooltip | string | TooltipContent props | — | Shown beside the icon when collapsed to icons, on desktop. |
The rest are structure, passing everything through: SidebarHeader, SidebarContent and SidebarFooter stack in the sidebar (the content scrolls); SidebarGroup pads a section; SidebarMenu and SidebarMenuItem are its <ul> and <li>; SidebarInset is the page's <main>; SidebarTrigger is the icon button that toggles it.
Guidelines
- The dashboard's one navigation — one flat menu, a handful of pages. Each item is an icon and a one-word noun: “Home”, “Clients”.
- Items navigate, so each renders a
Linkthroughrender; exactly one isisActive, set from the path. - Icons are Hugeicons, drawn by Glyph and hidden from assistive tech — the label names the page.
- The wash glides. Each menu draws the ghost Button's wash on two layers behind its items: one follows the pointer to the nearest item (quick, smooth-out; no blink at the gaps), one holds the current page and slides to the next (fast, as tabs slide). Both appear in place first, and with reduced motion they fade rather than travel. A separate
SidebarMenukeeps its own. - Only the parts the dashboard uses are exported from
shared/sidebar. Need a group label or a sub-menu? Re-export it there first, and document it here.