Shell
The window regions — root, header, body, aside, main, footer. Structure only, no aesthetic.
Usage
import {
ShellRoot, ShellHeader, ShellBody, ShellAside, ShellMain, ShellFooter,
} from "@kanzo-tech/ui";<ShellRoot>
<ShellHeader>{/* app chrome */}</ShellHeader>
<ShellBody>
<ShellAside aria-label="Navigation" side="start" width={240} />
<ShellMain>{/* the page */}</ShellMain>
<ShellAside aria-label="Inspector" side="end" width={280} />
</ShellBody>
<ShellFooter>{/* status */}</ShellFooter>
</ShellRoot>The regions carry no aesthetic
This is the design decision that matters most here, and it is deliberate.
A region places its children and separates itself from its neighbour. It has no height, no surface, no typography, no font size. Everything you can see inside one is yours.
There is no Toolbar and no StatusBar, and there will not be one. A thin strip with
start/center/end clusters is only a component if it also carries a height, a surface and a font
size — an IDE aesthetic, which is a product decision rather than a system one. A dense strip is
something you put in a region. The workspace showcase shows one;
copy it if you want that look.
This was decided the expensive way. Toolbar, StatusBar and TopBarUtility were three copies of
one strip; merging them into a generic ShellBar kept the fixed height, the card surface, the muted
ink and the eleven-pixel font — the IDE aesthetic of the component the owner had explicitly rejected.
Generalising an implementation while preserving a rejected appearance is not generalising.
What would reverse it: a caller who cannot express the strip they want by putting content in a region. Three attempts produced no such caller; the dense IDE strip now lives in the workspace showcase, where anyone who wants that look copies it.
Held by packages/ui/src/layouts/shell.tsx, the region set;
packages/ui/src/layouts/shell.test.tsx, "declare no role, so the call site owns the landmark" and
"impose no height, surface or typography of their own".
So a status strip is ordinary composition:
<ShellFooter
aria-label="Status"
className="h-[1.625rem] flex-row items-center gap-2 bg-card px-1.5 text-[11px] text-muted-foreground"
role="contentinfo"
>
<span className="min-w-0 flex-1">Ready</span>
<ToggleGroup aria-label="Panels" multiple onValueChange={onToggle} value={active}>
{/* … */}
</ToggleGroup>
</ShellFooter>Rules
Exactly one <main> per page. ShellMain renders it. Nested containers use <section> —
two <main> elements are a conformance error and make "skip to main content" ambiguous. The link
that phrase names is SkipNavLink, and ShellMain is what it lands on.
SidebarInset is a neutral offset <div> and carries no landmark. It is a styling wrapper — the
inset margin, radius and shadow — and the ShellMain placed inside it is what holds the region.
shadcn makes SidebarInset the <main> because it has no separate region layer; we do, which is
exactly why our answer differs from the one you will find upstream, and there is nothing that would
reverse it. This is the most-copied false claim in the repository: four documents have asserted
that SidebarInset renders the landmark, and a reader who follows them ships a page with no landmark
and an ambiguous skip link.
Held by packages/ui/src/layouts/shell.test.tsx, "renders exactly one <main>, and it is
ShellMain"; packages/ui/src/composites/sidebar.tsx, the comment above SidebarInset.
A shell has two legal shapes, and they do not mix. Either the header and footer go inside
SidebarInset, right of the rail — the shadcn model, and the canonical one here — or the sidebar is
in-flow (collapsible="none", or a ShellAside) and a spanning header is legitimate. Never both. A
fixed rail (collapsible="icon" or "offcanvas") starts at viewport top and paints over any
header that spans across it, so nothing reverses this either: it is a consequence of the rail's
positioning, not a preference.
Held by packages/ui/src/composites/sidebar.tsx, the rail variants;
packages/ui/src/layouts/shell.tsx, the regions.
Asides are landmarks. ShellAside renders <aside>, so two of them need aria-label to be
told apart. That is also why asides may repeat where <main> may not.
Logical properties, never physical. side="start" | "end" and border-e / border-s, never
left/right — one code path mirrors correctly under RTL.
No region declares a role. A top region is often banner, a bottom one often contentinfo,
a strip is neither, and a shell may have several — so assuming a landmark would mint duplicates.
Pass role and aria-label at the call site.
ShellRoot uses h-dvh, not h-screen. On mobile browsers 100vh includes the retracting
URL bar, so a shell sized with it is taller than the visible viewport and its bottom region sits
off screen.
Asides
side decides which edge carries the divider. width is the docked width in px — a computed
value the caller owns, which is why it is inline style rather than a class.
overlay turns the aside into a full-container drawer that floats over the content, for narrow
viewports. It has no divider, and width is ignored: it fills its container via inset-0.
Resizing is composed, not a prop. Wrap Ark's Splitter around the aside and the drag,
keyboard resizing and ARIA come from the machine. A resizable prop would mean re-implementing
behaviour Ark already ships.
API Reference
ShellAside
| Prop | Type | Default |
|---|---|---|
side | "start" | "end" | "start" |
overlay | boolean | false |
width | number | — |
Extends ComponentProps<typeof ark.aside>. side is mirrored to data-side and decides which
edge carries the divider; width is applied as an inline style — the sanctioned exception,
because it is a computed value the caller owns — and is ignored when overlay, where the aside
fills its container.
The other regions
ShellRoot, ShellHeader, ShellBody and ShellFooter have no props of their own and extend
ComponentProps<typeof ark.div>; ShellMain extends ComponentProps<typeof ark.main>.
asChild, ref, role and aria-label pass through — no region declares a landmark role
itself, so the call site names them.
ShellAsideProps is exported, so a wrapper can take the same props without restating them.