AppShell
See AppShell in Storybook for every variant, with live controls.
The application frame: a fixed navigation column, a fixed top bar, and a scrolling content region.
import { AppShell, Sidebar, Breadcrumbs } from "@kairosis/eidos";
<AppShell
sidebar={<Sidebar sections={sections} activeItem={active} onSelect={go} />}
header={<Breadcrumbs items={crumbs} onNavigate={go} />}
>
<RunsPage />
</AppShell>;
Props
| Prop | Type | Default | Description |
|---|---|---|---|
sidebar | ReactNode | — | The nav column. Becomes a drawer when compact |
header | ReactNode | — | The top bar's contents — AppShell supplies the bar itself |
children | ReactNode | — | The page. The only region that scrolls |
breakpoint | number | 768 | Viewport width (px) below which the sidebar collapses to a drawer |
contentPadding | number | 24 | Padding around the content region |
sidebarOpen | boolean | — | Controls the drawer. Omit to let AppShell manage it |
onSidebarOpenChange | (open: boolean) => void | — | |
sidebarLabel | string | "Navigation" | Accessible name for the drawer |
What it owns — and doesn't
It is layout only. It knows nothing about routing, page titles, auth, or what belongs in the header. An AppShell with opinions about those stops fitting the app, and gets fought or ejected.
What it does own is the height and scroll contract, which is fiddly enough to be worth encoding once:
Sidebarisheight: 100%and collapses to nothing without a sized flex parent. That contract was previously undocumented and enforced by nothing; the shell now provides it.min-width: 0on the content column. A flex item's defaultmin-widthisauto, meaning it refuses to shrink below its content — so one wideDataTablewould shove the whole page sideways instead of scrolling inside its own region.100dvh, not100vh. On mobile Safari the URL bar makes the visual viewport shorter than100vhclaims, and avh-sized shell hangs below the fold.
The content region is the only scroll container, so the sidebar and header stay put while the page moves. It's also the <main> landmark.
Responsive
Below breakpoint the sidebar becomes an overlay drawer and a menu button appears in the header. The drawer traps focus, closes on Escape or a press on the scrim, and returns focus to the menu button.
Crossing the breakpoint always closes the drawer — a stale open state must not leave it stranded over the layout, or spring it back open the next time the viewport narrows.
Guidelines
headeris the bar's contents, not the bar. AppShell draws the 52px bar, border and background; you fill it.- With no
headerand no drawer to open, the bar isn't rendered at all. - Pass
sidebarOpen/onSidebarOpenChangeif the drawer needs to close on navigation — AppShell can't know when a route changed.