/// Layout
AppShell
The application frame — an optional env strip and top bar over a sidebar + main body row, with an optional footer and a responsive mobile drawer.
import { AppShell } from "@protocore/pds";Basics
Fill the slots — env, topBar, sidebar, footer, and children for the main region. AppShell lays them out as a grid: env strip and top bar pinned on top, sidebar as a fixed column beside the main content, footer below.
Mobile drawer
Below the 860px brand breakpoint the sidebar becomes an off-canvas drawer. Control it with sidebarOpen / onSidebarOpenChange and wire a menu button in the top bar to toggle it; the click-away backdrop closes it.
When to use it
AppShell is the outermost frame for an application — a console or dashboard with persistent chrome. It owns the hard parts: the sticky env/top-bar stack, the sidebar-plus-main grid, and the responsive collapse to a drawer. Use it once, at the root of an app; compose the slots from the layout primitives (TopBar, Sidebar, Footer, EnvStrip — pass env and it renders the strip for you). It is not for marketing pages, which want a plain TopBar + Sections + Footer and no fixed rail. Keep sidebar open-state controlled at the app root so a menu button in the top bar and the backdrop stay in sync.
Usage
Do
- Use one AppShell at the root of an application to own its chrome.
- Pass env so the strip renders above the top bar automatically.
- Control sidebarOpen at the root and wire a top-bar menu button to it.
Don't
- Wrap marketing pages in AppShell — they want a plain TopBar and Sections.
- Nest AppShells or hand-build the sidebar/main grid yourself.
- Leave the mobile drawer uncontrolled if a top-bar button must also toggle it.
Accessibility
| Keys | Action |
|---|---|
| Tab | Move through the top bar, sidebar, main, and footer in DOM order |
| Enter / Space | Activate the focused control (e.g. the menu toggle) |
| Esc | Not handled by AppShell — wire it to onSidebarOpenChange(false) if you want Esc to close the drawer |
- The frame renders semantic landmarks: `<main>` for content and `<aside>` for the sidebar column.
- The mobile backdrop is `aria-hidden` and closes the drawer on click.
- Give a top-bar menu toggle an `aria-label` and reflect the drawer state with `aria-expanded` on your button.
AppShell props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Main content region. |
className | string | — | |
defaultSidebarOpen | boolean | false | Uncontrolled initial mobile sidebar open state. Default false. |
env | enum | — | When set, renders a sticky environment strip above the top bar. |
envLabel | ReactNode | — | Override the environment strip label. |
envMessage | ReactNode | — | Wayfinding message for the environment strip (grows it into its loud form). |
footer | ReactNode | — | The footer slot (e.g. a `<Footer>`), below the body. |
onSidebarOpenChange | ((open: boolean) => void) | — | Fires with the next open state when the mobile sidebar is toggled (e.g. by the click-away backdrop). |
sidebar | ReactNode | — | The sidebar slot (e.g. a `<Sidebar>`). A fixed column on desktop; an off-canvas drawer on mobile. |
sidebarOpen | boolean | — | Controlled mobile sidebar open state. Pair with `onSidebarOpenChange`. |
style | CSSProperties | — | |
topBar | ReactNode | — | The top bar slot (e.g. a `<TopBar>`), pinned above the body. |