Sidebar
An aside with header, content, and footer slots that morphs between an expanded column and an icon rail, and becomes one sticky horizontal strip under a 700px viewport. The app's top bar and its sidebar are the same component.
Three layouts
SidebarProvider holds the collapsed state and matches the same 700px media query the class names use, through matchMedia. One collapsed boolean and one 700px threshold resolve all three layouts. Expanded is the full column, collapsed is the icon rail, and strip is forced under the threshold whatever collapsed says. CSS paints the three; useSidebar().layout reports which one is live so a consumer's header, footer, or rail content can follow without re-deriving the query. matchMedia reads sidebarStripThreshold; the class names spell the same number as min-[700px]:, because Tailwind scans class names as literals and cannot read a JavaScript constant. The server has no viewport, so layout resolves at hydration and the aside leaves its width to CSS until it does.
App nav
The whole app shell: header with the trip name and the trigger, nav groups in the scrolling content, and a footer holding the avatar menu and the theme toggle. Collapse it and the column morphs to the rail — labels fade out, group labels fade to nothing but keep their space so the groups below them do not jump, and each label reappears in a tooltip on hover and on focus. The accessible name never depends on that tooltip: the label stays in the DOM, clipped rather than removed, so a screen reader reads the same nav in either layout. Items are the component's to reshape; header and footer are slots. Both become one rail-wide box with the same padding, and content too wide for it is the app's to swap on useSidebar().layout — this demo drops the trip name and the theme toggle in the rail, leaving the avatar menu on the centre line. The rail tooltip needs a string label; an item whose children are markup keeps its own visible text in the rail instead.
The strip
Under 700px the aside becomes one sticky top-0 row with border-b in place of border-r: header content leads, the nav scrolls horizontally with its groups flattened, and footer content trails. The trigger is gone, because there is nothing left to collapse. That is the whole of the app's top bar — the registry ships no separate header component (#128). Narrow the window below 700px and the demo above becomes this, with no change to its markup.
Items and the bar
SidebarItem takes an icon slot and its label as children, and reads active from aria-current="page" — on the item or on the element asChild renders, so a router link that already sets it needs nothing else. Idle is --muted-foreground at 36px, hover steps the background to --accent, and active is --foreground in medium with its icon and a 2px bar both in --indicator, the bar on the leading edge and on the bottom edge in the strip. The bar is one shared motion.span that slides between items on spring-bounce, so the sidebar has to stay mounted across routes for the slide to happen. Put it in a persistent layout route. A sidebar that remounts on every navigation is not wrong — it just draws the bar in place instead of moving it there. Items square off against the edges — no radius — and SidebarContent scrolls without a scrollbar, nudging the active item fully into view when it sits half outside the visible area, down the column or across the strip.
Widths and tokens
--sidebar-width is 18.125rem and --sidebar-width-collapsed is 3.5rem, both set on the root and overridable through className rather than through props. The component adds no tokens of its own: it paints --background, --border, --accent, --indicator, and --ring. The width morph runs on spring-settle because it displaces the pane beside it (ADR 0001), labels fade on --motion-fast, and the switch into the strip is a breakpoint and is not animated.
Accessibility
SidebarNav is a nav landmark and requires an aria-label, so a screen reader can tell the app nav from any other nav on the page. SidebarTrigger is an outline icon button carrying aria-expanded and aria-controls pointed at the aside, labelled "Collapse sidebar" or "Expand sidebar"; it takes its glyph as children, because the registry ships no icons. Tab reaches every item and the trigger, and there are no arrow keys. These are links in a landmark, not a menu. A focused item takes the same --accent background as a hovered one and draws no ring: the pointer and the keyboard land on the same mark, and the current page keeps its own — the bar and the --foreground label — so the two stay distinguishable.
The docs site still has its own sidebar
The nav on the left of this page is docs-site chrome, not this component. Rebuilding it on Sidebar is an open dogfooding question on the batch map (#113), not a decision this build made.