Browse docs
Browse docs
The top-level layout container. Wires header + nav (with the responsive desktop-inline / mobile-drawer dance) + main + footer in one shot. Body scroll lock when the mobile drawer is open, Escape closes, backdrop click closes. The standard "app chrome" pattern, distilled.
import { AppShell, TopBar, LeftNav } from '@dashforge/tw';
<AppShell
header={<TopBar start={<Logo />} end={<UserMenu />} />}
nav={<LeftNav items={NAV_ITEMS} />}
footer={<Footer />}
>
<YourPageContent />
</AppShell>Minimal — no nav, just header + main:
<AppShell header={<TopBar start={<Logo />} end={<UserMenu />} />}>
<YourPage />
</AppShell>Full setup (header + nav + main + footer):
<AppShell
header={<TopBar start={<Logo />} end={<UserMenu />} />}
nav={
<LeftNav
items={NAV_ITEMS}
activeId={activeNavId}
brand={<Brand />}
footer={<UserBlock />}
/>
}
footer={<Footer />}
>
<YourPage />
</AppShell>AppShell auto-switches the nav rendering based on viewport:
| Viewport | Nav rendering |
|---|---|
≥ md (768px) | Inline column on the left, always visible |
< md | Drawer (hidden by default); a hamburger appears in the header to open it |
Body scroll is locked while the drawer is open. Escape key closes. Tapping the backdrop closes. The header's hamburger button is rendered for you — you don't need to wire it.
If you want to control the drawer state explicitly (e.g. open it from somewhere else in your UI):
const [navOpen, setNavOpen] = useState(false);
<AppShell
header={<TopBar ...end={<Button onClick={() => setNavOpen(true)}>Menu</Button>} />}
nav={<LeftNav items={NAV_ITEMS} />}
navOpen={navOpen}
onNavOpenChange={setNavOpen}
>
<YourPage />
</AppShell><AppShell header={<TopBar start={<Logo />} end={<UserMenu />} />}>
<Container size="lg" py={12}>
<YourMarketingPage />
</Container>
</AppShell>When nav is omitted, the layout is single-column. No drawer logic, no hamburger.
<AppShell nav={<LeftNav items={...} />}>
<YourCanvasApp />
</AppShell>Skipping header is unusual but supported — for design tools / IDE-like layouts where the top is part of the canvas itself.
<AppShell
header={<TopBar ... />}
nav={<LeftNav ... />}
footer={
<Box variant="outlined" p={4}>
<Stack direction="row" justify="between">
<Typography variant="caption">© 2026 Your Company</Typography>
<Stack direction="row" gap={3}>
<a href="/terms">Terms</a>
<a href="/privacy">Privacy</a>
</Stack>
</Stack>
</Box>
}
>
<YourPage />
</AppShell>The footer sits below main (not below nav) — so on viewports with the nav column, the footer spans the full content width but not the nav rail.
| Prop | Type | Default | Description |
|---|---|---|---|
header | ReactNode | — | Top bar slot. Typically <TopBar>. |
nav | ReactNode | — | Left navigation slot. Typically <LeftNav>. When provided, the responsive desktop/drawer dance kicks in. |
footer | ReactNode | — | Footer slot, below main. |
children | ReactNode | — | The main page content. |
navOpen | boolean | — | Controlled drawer state. Pass with onNavOpenChange for full control. |
onNavOpenChange | (open: boolean) => void | — | Drawer open/close callback (fires on hamburger, Escape, backdrop click). |
slotProps | AppShellSlotProps | — | Per-slot overrides. |
sx | string | — | Utility-class override on root. |
root · header · nav · navMobile · main · footer · backdrop
md (768px) — the standard Tailwind breakpoint where desktop / mobile diverge ergonomically. Not configurable (yet). If you need a different breakpoint, wrap with your own useMediaQuery and conditionally render <AppShell> vs your custom layout.navOpen + onNavOpenChange for controlled mode (rare — usually only needed to programmatically open the drawer from outside the header).<body> gets overflow: hidden to prevent content scrolling underneath the drawer. Restored on close.aside, no <main> enforcement: AppShell renders semantic HTML where it makes sense (<aside> for the nav rail on desktop) but doesn't force a single semantic structure on you — the children stay in a regular <main> so you can include <article>, <section>, etc. inside.