App Shell
Top-level page scaffold. Five slots — Header, Nav, Aside, Main, Footer — arranged via CSS-grid template areas. Covers app dashboards, three-column workspaces, and marketing pages from one primitive.
Installation
import {
AppShell,
AppShellHeader,
AppShellNav,
AppShellAside,
AppShellMain,
AppShellFooter,
} from "@gradeui/ui"Usage
AppShell is a CSS-grid layout with fixed grid-area slots. The nav prop picks the template — none, top, side, or three-pane — and the slot components drop wherever they go in the JSX. Order doesn't matter; each slot has a fixed area.
Header and Footer always span full width, regardless of nav variant — that's what makes the same primitive work for marketing pages (Header / Main / Footer) and three-column workspaces (Header spanning a sidebar+aside+main grid).
It's intentionally just structure — no collapse state, no context, no runtime JS. Server-rendered, consumer-themeable. For user-adjustable column widths (drag-to-resize), use Resizable instead.
nav="side"
The classic dashboard shape — nav on the left, main filling the rest.
Dashboard
A placeholder for whatever your page's content is.
nav="top"
In-app nav bar above main — a tab strip or section nav row. Combine with maxWidth="container" on the main region to cap the content width.
Dashboard
A placeholder for whatever your page's content is.
nav="three-pane"
The Slack/Mail/Notion shape — narrow nav rail + fixed-width Aside (a list, channel picker, or page tree) + flex Main. The middle column's width comes from the --rds-app-shell-aside CSS variable (default 320px) — override per-screen via inline style or a parent class without forking the component.
Re: Q4 plan
Marketing layout (Header + Footer)
Header and Footer span full width and always sit at the very top / very bottom — independent of the nav variant. With nav="none" you get the canonical marketing shape: Header / Main / Footer.
Dashboard
A placeholder for whatever your page's content is.
Header + side nav + Footer
Header and Footer span all columns, with the side-nav body row in the middle. Useful when an app has site-wide chrome (org switcher, help menu) above and an in-app side nav for navigation.
Dashboard
A placeholder for whatever your page's content is.
nav="none"
No nav — useful for auth flows, single-screen prototypes, or any page where you don't need an in-app navigation rail.
Dashboard
A placeholder for whatever your page's content is.
Aside width
When using nav="three-pane", the Aside column's width comes from a CSS variable, not a prop. The default is 320px — fits a typical inbox / channel-list / file-tree comfortably. To override per-screen, set --rds-app-shell-aside on the shell element (or any ancestor):
{/* Inline style on the shell */}
<AppShell
nav="three-pane"
style={{ "--rds-app-shell-aside": "280px" } as React.CSSProperties}
>
…
</AppShell>
{/* Or via a parent class — useful for breakpoint-based switching */}
<div className="[--rds-app-shell-aside:240px] lg:[--rds-app-shell-aside:360px]">
<AppShell nav="three-pane">…</AppShell>
</div>Sticky header / nav / aside
AppShellHeader, AppShellNav and AppShellAside each take a sticky prop. Defaults: Nav true, Header and Aside false. Side and aside variants get h-screen self-scroll when sticky — so a long list scrolls inside its column, not the page.
User-adjustable columns
AppShell columns are static — the widths come from the grid template. If you want users to drag column dividers to resize, drop a ResizablePanelGroup inside an AppShellMain (or use it as the only child of nav="none") and let it manage the splits.
Composition
Drop any nav component into AppShellNav and any page content into AppShellMain. A typical productivity-app shell with site chrome, side nav, and a footer:
<AppShell nav="side">
<AppShellHeader sticky>
<OrgSwitcher /> <SearchInput /> <UserMenu />
</AppShellHeader>
<AppShellNav placement="side">
<Sidebar>
<SidebarHeader><Logo /></SidebarHeader>
<SidebarContent>
<SidebarSection title="Main">
<SidebarItem href="/" icon={<Home />}>Home</SidebarItem>
<SidebarItem href="/projects">Projects</SidebarItem>
</SidebarSection>
</SidebarContent>
</Sidebar>
</AppShellNav>
<AppShellMain>
<Stack gap="lg" className="p-6">
<PageHeader title="Dashboard" />
<DashboardGrid />
</Stack>
</AppShellMain>
<AppShellFooter>
<FooterLinks />
</AppShellFooter>
</AppShell>AppShell props
| Prop | Type | Default | Description |
|---|---|---|---|
| nav | "none" | "top" | "side" | "three-pane" | "none" | Layout structure. `top` puts an in-app nav row above main, `side` to the left, `three-pane` adds a fixed Aside column between Nav and Main, `none` hides nav entirely. Header and Footer always span full width regardless. |
| asChild | boolean | false | Render as the single child element via Radix Slot — stamp the shell layout onto an existing root tag without an extra wrapper div. |
| className | string | — | Extra classes merged onto the root element. |
AppShellHeader props
| Prop | Type | Default | Description |
|---|---|---|---|
| sticky | boolean | false | When true, the header sticks to the viewport top on scroll. Off by default — opt-in because marketing pages often prefer the header to scroll away. |
| asChild | boolean | false | Render as the child element via Radix Slot. |
AppShellNav props
| Prop | Type | Default | Description |
|---|---|---|---|
| placement | "none" | "top" | "side" | "top" | Should match the parent AppShell's `nav` prop — controls border side and sticky axis. For `nav="three-pane"`, use `placement="side"`. |
| sticky | boolean | true | When true, top nav sticks to the viewport top and side nav sticks with full-height self-scroll. |
| asChild | boolean | false | Render as the child element via Radix Slot. |
AppShellAside props
| Prop | Type | Default | Description |
|---|---|---|---|
| sticky | boolean | false | When true, Aside sticks to the viewport top with `h-screen` and self-scrolls — useful when the list is long. |
| asChild | boolean | false | Render as the child element via Radix Slot. |
AppShellMain props
| Prop | Type | Default | Description |
|---|---|---|---|
| maxWidth | "full" | "container" | "full" | `container` caps main at `max-w-7xl` with responsive padding — useful for marketing/docs pages. `full` leaves width unconstrained for dashboard layouts that want to fill the pane. |
| asChild | boolean | false | Render as the child element via Radix Slot. |
AppShellFooter props
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Render as the child element via Radix Slot. |
When to use
- As the root layout for any screen — app or marketing — at the top of a
layout.tsxor full-page route. - When you need a 3-column workspace (Slack/Mail/Notion shape) without hand-rolling
grid grid-cols-[auto_320px_1fr]— usenav="three-pane". - When you want columns the user can drag to resize, compose with Resizable inside the AppShell instead of relying on the grid.
- Pair Nav with Sidebar or TopMenu, and Main with Stack.
Sidecar
The Markdown sidecar Studio (and the Grade MCP server, when it ships) reads to understand this component — frontmatter, when- to-use guidance, and canonical examples. Authored once at packages/ui/components/ui/app-shell.md and shipped inside the published @gradeui/ui tarball.
---
name: AppShell
import: "@gradeui/ui"
role: layout
subcomponents: [AppShellNav, AppShellMain]
props:
- nav?: "none" | "top" | "side" (default "none") — layout structure. "top" puts the nav above main, "side" to the left, "none" hides it
- asChild?: boolean (default false) — render as the child element via Slot
- className?: string
- children: React.ReactNode
when_to_use: The top-level page scaffold for app-like layouts — any screen that needs a nav region plus a content region. Reach for AppShell instead of hand-rolled `grid grid-cols-[auto_1fr]` so the layout shape (top vs side nav, constrained vs full-width main) is a prop the settings panel can mutate. Drop a Stack of nav items into AppShellNav for the nav region; drop a Stack into AppShellMain for the page's vertical rhythm.
composes_with: [Stack, Row, Card, Button, Separator, any page content]
aliases: [app shell, page shell, layout, app layout, dashboard shell, scaffold, navigation split view, navigationsplitview, split view layout, safe area view, safeareaview]
notes: |
Three parts:
AppShell — <div> by default; sets the grid (nav=none|top|side)
AppShellNav — <nav> by default; props: placement ("top"|"side"|"none", match AppShell.nav), sticky (boolean, default true)
AppShellMain — <main> by default; props: maxWidth ("full"|"container", default "full")
All three support asChild and emit data-gds-part ("app-shell", "app-shell-nav", "app-shell-main").
Pure structure — no collapse state, no context. Server-renders cleanly.
For nav placement="side" + sticky=true the nav gets h-screen + self-scroll, so long nav lists don't push main down.
---
```jsx
// Side nav + full-width main — the classic dashboard shape.
<AppShell nav="side">
<AppShellNav placement="side">
{/* nav items — Stack of Buttons, a Sidebar, etc. */}
</AppShellNav>
<AppShellMain>
<Stack gap="lg" className="p-6">
{/* page content */}
</Stack>
</AppShellMain>
</AppShell>
```
```jsx
// Top nav + constrained content — marketing / docs / settings pages.
<AppShell nav="top">
<AppShellNav placement="top">
<Row justify="between" align="center" className="px-6 py-3">
{/* logo + nav buttons */}
</Row>
</AppShellNav>
<AppShellMain maxWidth="container">
<Stack gap="lg" className="py-8">
{/* page content */}
</Stack>
</AppShellMain>
</AppShell>
```
```jsx
// No nav — just a shell for a single-screen prototype.
<AppShell nav="none">
<AppShellMain maxWidth="container">
{/* page content */}
</AppShellMain>
</AppShell>
```