The signed-in frame in four layouts: an expanded frosted sidebar, a collapsible icon rail, a header-only top nav, and a drawer on narrow screens.
SidebarShell is the dashboard frame. The page field is painted once on the whole frame; the sidebar and top bar are frosted glass over it, so the chrome carries a whitened version of the field and the screen reads as one surface. Only <main> scrolls, so the chrome never moves and its blur always has content passing beneath it.
layout picks the frame. sidebar (the default) is the expanded glass L. rail is the sidebar collapsed to icons: labels are visually hidden (each item keeps its title as a tooltip), counts sit on the icon and the logo corner shows the compact mark (logoCompact). header drops the sidebar: the logo, the nav as horizontal links and topbarRight share one frosted top bar, and the page sits below. collapsible adds a panel button to the top bar that folds the sidebar to the rail and back; storageKey remembers the choice in localStorage, read after mount so the server and client render the same.
Below 900px of the frame's own width (600px when contained; set mobileBelow to change it) the sidebar becomes an off-canvas drawer: a menu button in the top bar opens it, focus moves in and is held there, and Esc, the scrim or picking a link closes it and returns focus to the button. mobileDrawer={false} keeps the icon rail on narrow screens instead. forceMobile and defaultDrawerOpen exist for previews like the one here.
The logo corner is flush, square and exactly the top bar's height. By default (logoCorner="glass") it is the same frosted glass as the sidebar and top bar, so the chrome reads as one continuous L. logoCorner="fill" opts in to a set colour or gradient behind the logo (--logo-corner-bg, with --logo-corner-ink for the default wordmark), for brands whose guidelines require one. Pass your logo as an element (logo) or an image URL (logoSrc); without either it shows a sample glyph and "Acme", the placeholder brand. strip adds a full-width band across the top (an admin session, a staging site), on --admin-strip-bg. The active nav item is the solid brand-gradient pill, the one gradient on the chrome.
Every chrome colour reads a token: --chrome-sidebar, --chrome-topbar, --chrome-shadow, --sidebar-bg, --topbar-bg, --sidebar-ink, --sidebar-hover-bg, --sidebar-active-bg and -ink, --sidebar-rule, --shell-sidebar-width, --shell-rail-width, --shell-drawer-width, plus --shell-logo (a url()). It is presentation only: no auth in here.
Installation
npx shadcn@latest add KezLahd/rime/sidebar-shellAlso installs: base, icon-button.
Usage
import { SidebarShell } from "@/components/shell/SidebarShell";
<SidebarShell
navLabel="Main"
logo={<img src="/logo.svg" alt="Acme" width={140} height={32} />}
nav={[
{ href: "/dashboard", label: "Dashboard", icon: <IconHome size={16} />, exact: true },
{ href: "/projects", label: "Projects", icon: <IconClipboard size={16} />, count: 2 },
]}
title="Projects"
topbarRight={<Avatar name="Jane Cooper" size="sm" />}
collapsible
storageKey="app-sidebar-collapsed"
>
{children}
</SidebarShell>Examples
Sidebar expanded (logo corner: match glass)
Collapsible, remembered
Collapsed to an icon rail
Header only (top nav)
Mobile drawer
Logo corner: brand fill
With a context strip
API
Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.
SidebarShell
| Prop | Type | Default | Description |
|---|---|---|---|
| childrenRequired | ReactNode | ||
| navRequired | ReadonlyArray<SidebarNavItem> | ||
| bottomNav | ReadonlyArray<SidebarNavItem> | Pinned to the bottom of the sidebar (settings, sign out). Not shown in the header layout. | |
| navLabelRequired | string | ||
| title | ReactNode | Page title shown in the top bar (sidebar and rail layouts). | |
| topbarRight | ReactNode | Right side of the top bar: search, workspace switcher, profile. | |
| strip | ReactNode | Full-width strip across the top of the frame, logo corner included (the admin context). | |
| footer | ReactNode | ||
| layout | SidebarShellLayout | "sidebar" | sidebar (the default) = the expanded frosted sidebar and top bar, one glass L. rail = the sidebar collapsed to an icon rail. header = no sidebar: the logo, the nav as horizontal links and topbarRight in one frosted top bar, with the page below. |
| collapsible | boolean | Shows a toggle in the top bar that collapses the sidebar to the rail and back. The starting state is the layout prop (rail = collapsed) unless defaultCollapsed says otherwise. Ignored in the header layout. | |
| defaultCollapsed | boolean | Start collapsed (as the rail). Defaults to layout === "rail". | |
| storageKey | string | Persists the collapsed state in localStorage under this key. Read after mount. | |
| onCollapsedChange | (collapsed: boolean) => void | ||
| mobileDrawer | boolean | true | Below the mobile breakpoint, the sidebar becomes an off-canvas drawer opened from a menu button in the top bar. On by default; false keeps the icon rail on narrow screens instead. |
| mobileBelow | number | The frame width (px) below which the drawer (or rail if mobileDrawer is false) applies. Defaults to 600, or 420 when contained. | |
| tabletBelow | number | The frame width (px) below which the sidebar auto-collapses to the icon rail (tablet mode). A chevron at the bottom of the rail toggles back to the full width. Defaults to 1024, or 760 when contained. Set below mobileBelow to disable tablet mode. | |
| forceMobile | boolean | Treat the frame as narrow whatever its width: the docs use it to show the drawer at desktop width. | |
| defaultDrawerOpen | boolean | Open the drawer on first render, without moving focus (a static docs preview). | |
| logo | ReactNode | The logo corner's content: your logo as an element (an <img>, an inline SVG, a wordmark). Defaults to a sample glyph and "Acme", the placeholder brand. | |
| logoCompact | ReactNode | The logo for the rail's narrow corner: a mark without the wordmark. Defaults to the glyph. | |
| logoCorner | "glass" | "fill" | "glass" | The logo corner's ground. glass (the default) = the same frosted glass as the sidebar and top bar, one continuous L. fill = a set colour behind the logo, from --logo-corner-bg (ink --logo-corner-ink), for brands whose guidelines require one. |
| logoSrc | string | A logo image URL (or data URL) for the corner, in place of `logo`. Up to 176px wide. | |
| logoAlt | string | Alt text for logoSrc. Required in practice whenever logoSrc is set. | |
| activeHref | string | Overrides the pathname match; used by the docs previews. | |
| contained | boolean | Fill the parent instead of the viewport. The docs render the shell inside a fixed-height frame so the sticky chrome can be seen working. |
Theme tokens
Per-component tokens are undeclared by default, so the default shown is the fallback the CSS uses. Set one at :root (or in theme.css) to restyle every instance. Live values are read from this page, so they follow the Rime Studio.
| Token | Default | Live value |
|---|---|---|
| --brand-height | var(--topbar-height) | … |
| --chrome-backdrop | var(--blur-chrome) saturate(160%) | … |
| --logo-corner-art | none | … |
| --logo-corner-art-opacity | 0 | … |
| --logo-corner-ink | var(--ink-inverse) | … |
| --page-art | none | … |
| --page-art-opacity | 0 | … |
| --shell-drawer-width | 264px | … |
| --shell-logo | normal | … |
| --shell-logo-width | 176px | … |
| --shell-rail-width | 68px | … |
| --shell-sidebar-width | 212px | … |
| --shell-title-size | 26px | … |
| --sidebar-active-badge-bg | rgba(var(--rgb-white), 0.25) | … |
| --sidebar-active-bg | var(--brand-gradient) | … |
| --sidebar-active-ink | var(--ink-inverse) | … |
| --sidebar-active-shadow | var(--glow-md), inset 0 1px 0 rgba(var(--rgb-white), 0.18) | … |
| --sidebar-art | none | … |
| --sidebar-art-opacity | 0 | … |
| --sidebar-bg | var(--chrome-sidebar) | … |
| --sidebar-drawer-bg | var(--modal-surface) | … |
| --sidebar-hover-bg | rgba(var(--rgb-white), 0.65) | … |
| --sidebar-ink | var(--ink-muted) | … |
| --sidebar-ink-hover | var(--ink-heading) | … |
| --sidebar-item-radius | var(--r-lg) | … |
| --sidebar-rule | rgba(var(--rgb-brand-deep), 0.08) | … |
| --sidebar-shadow | 8px 0 28px rgba(var(--rgb-brand-deep), 0.12), 1px 0 3px rgba(var(--rgb-contact), 0.06) | … |
| --topbar-bg | var(--chrome-topbar) | … |
| --topbar-shadow | var(--chrome-shadow) | … |
System tokens it reads
| Token | Default | Live value |
|---|---|---|
| --admin-strip-bg | tokens.css | … |
| --blur-chrome | tokens.css | … |
| --blur-scrim | tokens.css | … |
| --brand-gradient | tokens.css | … |
| --chrome-shadow | tokens.css | … |
| --chrome-sidebar | tokens.css | … |
| --chrome-topbar | tokens.css | … |
| --danger-base-solid | tokens.css | … |
| --density | tokens.css | … |
| --dur | tokens.css | … |
| --dur-slow | tokens.css | … |
| --ease | tokens.css | … |
| --ease-out | tokens.css | … |
| --focus-ring | tokens.css | … |
| --font-display | tokens.css | … |
| --glow-md | tokens.css | … |
| --glow-sm | tokens.css | … |
| --ink-brand | tokens.css | … |
| --ink-heading | tokens.css | … |
| --ink-inverse | tokens.css | … |
| --ink-muted | tokens.css | … |
| --line | tokens.css | … |
| --logo-corner-bg | tokens.css | … |
| --modal-blur | tokens.css | … |
| --modal-surface | tokens.css | … |
| --page-background | tokens.css | … |
| --page-base | tokens.css | … |
| --r-full | tokens.css | … |
| --r-lg | tokens.css | … |
| --r-md | tokens.css | … |
| --r-sm | tokens.css | … |
| --r-xl | tokens.css | … |
| --rgb-brand-deep | tokens.css | … |
| --rgb-contact | tokens.css | … |
| --rgb-white | tokens.css | … |
| --scrim | tokens.css | … |
| --shadow-float | tokens.css | … |
| --shadow-modal | tokens.css | … |
| --surface-solid | tokens.css | … |
| --text-micro | tokens.css | … |
| --text-small | tokens.css | … |
| --topbar-height | tokens.css | … |
| --tracking-display | tokens.css | … |
Accessibility
- A skip link ("Skip to content") is the first focusable element and targets <main id="main">.
- The nav is a labelled <nav> (navLabel) in every layout; the current item has aria-current="page". In the rail the labels stay in the accessibility tree (visually hidden) and each item has a title tooltip.
- The collapse toggle is a real button with aria-expanded and a label that says what it will do ("Collapse sidebar", "Expand sidebar").
- The drawer's menu button has aria-expanded and aria-controls. While open, focus is held in the drawer, the page behind is inert, and Esc or the scrim closes it and returns focus to the button. A closed drawer is inert, so it is out of the tab order.
- The top bar title is not a heading: each page owns its h1.
- contained renders a <div> instead of <main>, so a preview never adds a second main landmark.
Do and don't
Do
- Gate access inside each page.tsx, never in the layout that renders the shell (a layout gate leaks the page into the RSC payload).
- Give the rail a compact mark (logoCompact) when your logo is a wide wordmark.
- Use the header layout for products with five or fewer top-level sections and no deep navigation.
- Keep account links in a profile menu in the top bar, not at the sidebar's foot.
Don't
- Don't give the sidebar a hue of its own; it is whitened glass over the same field.
- Don't add a second gradient to the chrome; the active pill is the one.
- Don't hide navigation behind the drawer on wide screens; it is for narrow frames only.
For AI agents
This page as markdown, rendered from the same registry entry. Point an agent at it (or add .md to this page's URL), or at /llms.txt for the index.
Open sidebar-shell.mdRelated: page-header, glass-panel