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.

Users
1,248
Revenue
$24.3k

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.

Users
1,248
Revenue
$24.3k

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.

Elena Okafor

Re: Q4 plan

Hey — taking another look at the roadmap. The Q4 cuts feel right but I'd love to talk through the timing on…

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.

Acme

Dashboard

A placeholder for whatever your page's content is.

Users
1,248
Revenue
$24.3k
© 2026 Acme Inc.
PrivacyTermsStatus

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.

Acme

Dashboard

A placeholder for whatever your page's content is.

Users
1,248
Revenue
$24.3k
© 2026 Acme Inc.
PrivacyTermsStatus

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.

Users
1,248
Revenue
$24.3k

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

PropTypeDefaultDescription
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.
asChildbooleanfalseRender as the single child element via Radix Slot — stamp the shell layout onto an existing root tag without an extra wrapper div.
classNamestringExtra classes merged onto the root element.

AppShellHeader props

PropTypeDefaultDescription
stickybooleanfalseWhen 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.
asChildbooleanfalseRender as the child element via Radix Slot.

AppShellNav props

PropTypeDefaultDescription
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"`.
stickybooleantrueWhen true, top nav sticks to the viewport top and side nav sticks with full-height self-scroll.
asChildbooleanfalseRender as the child element via Radix Slot.

AppShellAside props

PropTypeDefaultDescription
stickybooleanfalseWhen true, Aside sticks to the viewport top with `h-screen` and self-scrolls — useful when the list is long.
asChildbooleanfalseRender as the child element via Radix Slot.

AppShellMain props

PropTypeDefaultDescription
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.
asChildbooleanfalseRender as the child element via Radix Slot.

AppShellFooter props

PropTypeDefaultDescription
asChildbooleanfalseRender 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.tsx or full-page route.
  • When you need a 3-column workspace (Slack/Mail/Notion shape) without hand-rolling grid grid-cols-[auto_320px_1fr] — use nav="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>
```