Skip to main content

AppShell

Interactive

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

PropTypeDefaultDescription
sidebarReactNodeThe nav column. Becomes a drawer when compact
headerReactNodeThe top bar's contents — AppShell supplies the bar itself
childrenReactNodeThe page. The only region that scrolls
breakpointnumber768Viewport width (px) below which the sidebar collapses to a drawer
contentPaddingnumber24Padding around the content region
sidebarOpenbooleanControls the drawer. Omit to let AppShell manage it
onSidebarOpenChange(open: boolean) => void
sidebarLabelstring"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:

  • Sidebar is height: 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: 0 on the content column. A flex item's default min-width is auto, meaning it refuses to shrink below its content — so one wide DataTable would shove the whole page sideways instead of scrolling inside its own region.
  • 100dvh, not 100vh. On mobile Safari the URL bar makes the visual viewport shorter than 100vh claims, and a vh-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

  • header is the bar's contents, not the bar. AppShell draws the 52px bar, border and background; you fill it.
  • With no header and no drawer to open, the bar isn't rendered at all.
  • Pass sidebarOpen/onSidebarOpenChange if the drawer needs to close on navigation — AppShell can't know when a route changed.