Rime

SidebarShell

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-shell

Also 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)

Projects

Page content scrolls here, under the frosted top bar.

Collapsible, remembered

The panel button folds the sidebar to the icon rail and back. storageKey keeps the choice across reloads.

Projects

Press the panel button in the top bar: the sidebar folds to an icon rail, and the choice is remembered on reload.

Collapsed to an icon rail

layout="rail" starts collapsed. With collapsible it can still be expanded.

Projects

Page content scrolls here, under the frosted top bar.

Header only (top nav)

No sidebar: the logo, the nav and topbarRight share one frosted bar.
Acme

Page content scrolls here, under the frosted top bar.

Mobile drawer

Below 900px the sidebar is a drawer from the menu button. Shown open here with forceMobile and defaultDrawerOpen.

Projects

On a narrow screen the sidebar is a drawer from the menu button. Esc or the scrim closes it.

Logo corner: brand fill

Opt in for brands that require a set colour behind the logo. Set the colour with --logo-corner-bg (a colour or a gradient).

Projects

Page content scrolls here, under the frosted top bar.

With a context strip

Admin session

Overview

Page content scrolls here, under the frosted top bar.

API

Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.

SidebarShell

SidebarShell props
PropTypeDefaultDescription
childrenRequiredReactNode
navRequiredReadonlyArray<SidebarNavItem>
bottomNavReadonlyArray<SidebarNavItem>Pinned to the bottom of the sidebar (settings, sign out). Not shown in the header layout.
navLabelRequiredstring
titleReactNodePage title shown in the top bar (sidebar and rail layouts).
topbarRightReactNodeRight side of the top bar: search, workspace switcher, profile.
stripReactNodeFull-width strip across the top of the frame, logo corner included (the admin context).
footerReactNode
layoutSidebarShellLayout"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.
collapsiblebooleanShows 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.
defaultCollapsedbooleanStart collapsed (as the rail). Defaults to layout === "rail".
storageKeystringPersists the collapsed state in localStorage under this key. Read after mount.
onCollapsedChange(collapsed: boolean) => void
mobileDrawerbooleantrueBelow 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.
mobileBelownumberThe frame width (px) below which the drawer (or rail if mobileDrawer is false) applies. Defaults to 600, or 420 when contained.
tabletBelownumberThe 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.
forceMobilebooleanTreat the frame as narrow whatever its width: the docs use it to show the drawer at desktop width.
defaultDrawerOpenbooleanOpen the drawer on first render, without moving focus (a static docs preview).
logoReactNodeThe 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.
logoCompactReactNodeThe 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.
logoSrcstringA logo image URL (or data URL) for the corner, in place of `logo`. Up to 176px wide.
logoAltstringAlt text for logoSrc. Required in practice whenever logoSrc is set.
activeHrefstringOverrides the pathname match; used by the docs previews.
containedbooleanFill 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.

SidebarShell per-component tokens
TokenDefaultLive value
--brand-heightvar(--topbar-height)…
--chrome-backdropvar(--blur-chrome) saturate(160%)…
--logo-corner-artnone…
--logo-corner-art-opacity0…
--logo-corner-inkvar(--ink-inverse)…
--page-artnone…
--page-art-opacity0…
--shell-drawer-width264px…
--shell-logonormal…
--shell-logo-width176px…
--shell-rail-width68px…
--shell-sidebar-width212px…
--shell-title-size26px…
--sidebar-active-badge-bgrgba(var(--rgb-white), 0.25)…
--sidebar-active-bgvar(--brand-gradient)…
--sidebar-active-inkvar(--ink-inverse)…
--sidebar-active-shadowvar(--glow-md), inset 0 1px 0 rgba(var(--rgb-white), 0.18)…
--sidebar-artnone…
--sidebar-art-opacity0…
--sidebar-bgvar(--chrome-sidebar)…
--sidebar-drawer-bgvar(--modal-surface)…
--sidebar-hover-bgrgba(var(--rgb-white), 0.65)…
--sidebar-inkvar(--ink-muted)…
--sidebar-ink-hovervar(--ink-heading)…
--sidebar-item-radiusvar(--r-lg)…
--sidebar-rulergba(var(--rgb-brand-deep), 0.08)…
--sidebar-shadow8px 0 28px rgba(var(--rgb-brand-deep), 0.12), 1px 0 3px rgba(var(--rgb-contact), 0.06)…
--topbar-bgvar(--chrome-topbar)…
--topbar-shadowvar(--chrome-shadow)…

System tokens it reads

SidebarShell system tokens
TokenDefaultLive value
--admin-strip-bgtokens.css…
--blur-chrometokens.css…
--blur-scrimtokens.css…
--brand-gradienttokens.css…
--chrome-shadowtokens.css…
--chrome-sidebartokens.css…
--chrome-topbartokens.css…
--danger-base-solidtokens.css…
--densitytokens.css…
--durtokens.css…
--dur-slowtokens.css…
--easetokens.css…
--ease-outtokens.css…
--focus-ringtokens.css…
--font-displaytokens.css…
--glow-mdtokens.css…
--glow-smtokens.css…
--ink-brandtokens.css…
--ink-headingtokens.css…
--ink-inversetokens.css…
--ink-mutedtokens.css…
--linetokens.css…
--logo-corner-bgtokens.css…
--modal-blurtokens.css…
--modal-surfacetokens.css…
--page-backgroundtokens.css…
--page-basetokens.css…
--r-fulltokens.css…
--r-lgtokens.css…
--r-mdtokens.css…
--r-smtokens.css…
--r-xltokens.css…
--rgb-brand-deeptokens.css…
--rgb-contacttokens.css…
--rgb-whitetokens.css…
--scrimtokens.css…
--shadow-floattokens.css…
--shadow-modaltokens.css…
--surface-solidtokens.css…
--text-microtokens.css…
--text-smalltokens.css…
--topbar-heighttokens.css…
--tracking-displaytokens.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.md

Related: page-header, glass-panel