Sidebar anatomy
What this solves
Discovery and gestalt hierarchy. Grouping and per-group counts let a reader find a destination without scanning every item, and the collapsed rail trades labels for room without losing the ability to find anything, since every link keeps a tooltip.
Use it when
- The primary workspace rail in either app.Speedway's WorkspaceNav (WorkspaceNav.tsx:320-352), the kit Sidebar with the app's own nav data.
- A narrow viewport or an explicit collapse.Any page below the lg breakpoint: the rail always renders icon-only there, whatever collapsed says.
Rendered
Fixture data; check both themes.patterns/sidebar-anatomy/example.tsxCopy this into a fresh route and it renders as above.
"use client";import { useState } from "react";import { Sidebar, type NavGroup } from "@versable-git/ui";// The rail as the apps assemble it: grouped nav, a per-group count that// renders only when known, a per-item count that renders only when known,// and an identity row riding the footer slot. Collapse is driven by the// kit's own toggle, the same hamburger a real rail exposes.const nav: NavGroup[] = [ { label: "Catalog", count: 15, items: [ { label: "Home", href: "#home", Icon: "Dashboard" }, { label: "Jobs", href: "#jobs", Icon: "Table" }, { label: "Review", href: "#review", Icon: "Warning" }, ], }, { label: "Modules", items: [ { label: "Workflow", href: "#workflow", Icon: "Refresh" }, { label: "Scrape", href: "#scrape", Icon: "CloudUpload" }, ], },];const itemCounts: Record<string, number> = { "#jobs": 12, "#review": 3 };export function SidebarAnatomy() { const [collapsed, setCollapsed] = useState(false); return ( <div className={`border-base-300 bg-base-100 h-96 overflow-hidden rounded-lg border transition-[width] duration-200 ${collapsed ? "w-16" : "w-64"}`} > <Sidebar nav={nav} activeHref="#jobs" collapsed={collapsed} onToggleCollapsed={() => setCollapsed((v) => !v)} renderLink={({ href, className, children, ...rest }) => ( <a href={href} className={className} {...rest}> <span className="flex w-full items-center justify-between gap-2"> <span className="flex items-center gap-2">{children}</span> {itemCounts[href ?? ""] != null ? ( <span className="text-base-content/45 text-xs tabular-nums">{itemCounts[href ?? ""]}</span> ) : null} </span> </a> )} footer={ <span className="flex items-center gap-2 px-1"> <span className="bg-primary/15 text-primary flex size-7 items-center justify-center rounded-full text-xs font-semibold"> JD </span> <span className="flex min-w-0 flex-col"> <span className="truncate text-sm font-medium">Jordan Diaz</span> <span className="text-base-content/50 truncate text-xs">jordan@acme.test</span> </span> </span> } /> </div> );}Where it ships
speedway/app/components/WorkspaceNav.tsx:320-352the kit Sidebar carrying the app's NavGroup data, a router-aware renderLink, and the account footerspeedway/app/components/WorkspaceNav.tsx:71-99the RailLink adapter: prefetch on intent, and revalidate instead of navigate when clicking the current pagespeedway/app/components/WorkspaceNav.tsx:104-131RailRow: the pending spinner appears only after a 120ms grace period
App-specific: The real renderLink is a router Link with prefetch and a pending spinner, counts come from live queries (no skeleton fallback, deliberately), and collapse state lives in an app context. The frame here bounds the rail for display; apps mount it in the shell.