Build the operator app shell: sidebar, top bar, content
By the end of this recipe you'll have the chrome every Huchu operator app lives inside — a 252 px sidebar with brand mark, search, primary nav, tenant switcher and sign-out; a top bar with breadcrumbs and a bell; a content area that takes the rest. Plus collapse-to-icon-rail, persisted per user, and a Cmd+K shortcut.
Overview
One <AppShell> wraps your whole app. Compose with AppShell.Sidebar, AppShell.Topbar and AppShell.Main — the rest is your pages.
The sidebar app shell is the chrome a cashier, bursar or plant operator sees every working day. The job of this recipe is to make it boring in the right way — the brand mark, the search, the nav, the active state and the sign-out menu always live exactly where the operator's eye expects them, on every screen, in every app you ship. That sameness is what lets a bursar new to the inventory app find "Receipts" in three seconds.
The shell handles three responsibilities: layout (252 px sidebar on desktop, icon-rail at narrower widths, off-canvas drawer on mobile); state (collapsed/expanded persisted to localStorage, route-aware active nav, tenant pinned to the topbar); and shortcuts (Cmd+K for the command palette, Cmd+B to toggle the sidebar). Everything else — page content, breadcrumbs, page header — is rendered by whichever route is active.
Skip this recipe if your app is single-purpose and a one-page UI with a sticky top action bar is enough — adding a sidebar to a single-screen app just adds a column of empty space and a navigation muscle the user will never need.
What you'll build
Three breakpoints, one component. The shell adapts; your pages don't move.
You can see this live in
Portal demos that wire this shell in context. Open one to see the sidebar in action on a real workflow.
Required pieces
Everything this recipe pulls from @corelithzw/react. Click any to see its reference page.
@corelithzw/react exports used here: AppShell, AppShell.Sidebar, AppShell.Topbar, AppShell.Main, NavItem, NavGroup, Avatar, DropdownMenu, Input, Kbd, CommandPalette. The AppShell.* dot-namespace is the published-API convention for shells — every shell recipe follows the same shape.
Step-by-step build
Lay out the three slots and persist the collapsed state
The shell is grid + slot. AppShell takes three children with named slots; it handles the responsive grid so your pages never need to. Persist the collapsed bit to localStorage with a single hook — the operator stays in their preferred config across sessions, which is the single most-requested shell behaviour.
collapsed → localStorage. Survives sign-out.
Mobile shifts the Sidebar into an off-canvas drawer automatically.
Switch to the Code tab to see the snippet.
Compose the sidebar: brand, search, nav, tenant switcher, sign-out
The sidebar reads top-to-bottom in the same order on every Huchu app: brand → search → primary nav → spacer → tenant + user menu. Use NavItem with the route's match so the active state lights up automatically — no useLocation sprinkled across the tree. The tenant switcher and sign-out live in a single DropdownMenu anchored to the user avatar.
Compose the topbar: breadcrumb, bell, command palette trigger
The topbar is the one piece of chrome that knows what page you're on. Render a breadcrumb on the left (route-driven), a search-button on the right that opens the same command palette as Cmd+K, and a bell that shows unread notifications. Keep it under 56 px tall — anything more eats content the operator came here to see.
Wire the keyboard shortcuts: Cmd+K, Cmd+B, Esc
Three shortcuts cover 95% of operator nav: Cmd+K opens the command palette, Cmd+B toggles the sidebar, Esc closes whichever overlay is open. Bind them at the shell level with one useEffect; never re-bind in pages or you'll get drift the day someone forgets to unmount a listener.
Final composition
The whole Shell component, assembled. Sidebar slots, topbar slots, palette, shortcuts, persistence. Drop your route output into <Shell> and every page inherits the chrome.
Park Centre · today
Three tills open, one cashier on lunch.
$ 2,816
147
$ 19.16
Variations
Three common forks. Each is a small diff from the final composition — the slots stay the same shape.
Multi-site owner
Owner of three sites. Topbar carries a site picker; sidebar nav groups by site. The site they're focused on is part of the chrome, not a setting.
<AppShell.Topbar.Pickers>
<SitePicker
value={siteId}
options={sites}
onChange={setSite}
/>
</AppShell.Topbar.Pickers>
Nested settings shell
Inside Settings, the content column splits into a section rail + content. Reuse AppShell.Main as the host; render a SettingsShell inside.
<AppShell.Main>
<SettingsShell sections={SETTINGS}>
{/* settings page */}
</SettingsShell>
</AppShell.Main>
Focus mode
A workflow (close shift, post payroll). Hide the sidebar, swap the topbar for a brand + step header. Same shell, two props.
<AppShell mode="focus">
<AppShell.Topbar.Step
title="Close shift S-2841"
current={2} total={4}
/>
<AppShell.Main>{workflow}</AppShell.Main>
</AppShell>
Accessibility
What this recipe takes care of for you. Each item is something a screen-reader or keyboard user will notice.
- Landmarks for free.
AppShell.Sidebarrenders as<nav aria-label="Primary">;AppShell.Mainas<main>;AppShell.Topbaras<header>. Screen-reader users can jump straight to nav or content with rotor. - Active route is announced.
NavItemrendersaria-current="page"on the matching route, so SR users hear "Overview, current page" instead of just "Overview". - Collapse is keyboard-toggleable. Cmd/Ctrl+B toggles the sidebar; collapse-button has a real
aria-pressed+aria-controls. - Search input has a kbd hint and focuses the palette. Focusing the input opens the same palette as Cmd+K — keyboard-only users don't have to remember the shortcut.
- Tenant menu is a real menu.
DropdownMenuusesrole="menu"; arrow keys move between items, Enter selects, Esc closes and returns focus to the trigger. - Bell announces the count.
aria-label="Notifications (3 unread)", not just a bell icon — SR users hear the number. - Mobile drawer traps focus. When the off-canvas sidebar is open, Tab cycles inside it; Esc closes it and returns focus to the hamburger trigger.