App shell
A CSS grid with named areas — header, sidebar, main, footer — plus a small React context that wires <Navbar.MobileToggle> to the sidebar drawer. The composed pieces (navbar, sidebar, footer) also work standalone.
Anatomy
Section titled “Anatomy”+--------------------------------------------+| navbar |+----------+---------------------------------+| | || sidebar | main || (opt.) | || | |+----------+---------------------------------+| footer (opt.) |+--------------------------------------------+Quick start
Section titled “Quick start”<div class="app-shell" style="min-height: 16rem; --color-system-accent: var(--color-purple-600)"> <header class="navbar"> <div class="navbar-brand"> <span class="brand-tile" aria-hidden>A</span> Acme </div> </header> <main class="app-shell-main" style="padding: 1rem">Page content</main></div><AppShell systemAccent="var(--color-purple-600)" style={{ minHeight: "16rem" }}> <Navbar> <Navbar.Brand> <BrandTile monogram="A" /> Acme </Navbar.Brand> </Navbar> <AppShell.Main style={{ padding: "1rem" }}>Page content</AppShell.Main></AppShell>Add hasSidebar for a two-column grid; a <Footer> drops into the bottom row automatically.
<AppShell hasSidebar systemAccent="var(--color-purple-600)" style={{ minHeight: "20rem" }}> <Navbar> <Navbar.Brand> <BrandTile monogram="A" /> Acme </Navbar.Brand> </Navbar> <Sidebar> <Sidebar.Nav> <Sidebar.Item href="#" active icon={IconHome}> Dashboard </Sidebar.Item> <Sidebar.Item href="#" icon={IconReceipt}> Orders </Sidebar.Item> </Sidebar.Nav> </Sidebar> <AppShell.Main style={{ padding: "1rem" }}>Page content</AppShell.Main> <Footer> <Footer.Meta>© Acme</Footer.Meta> </Footer></AppShell>Navbar
Section titled “Navbar”48px-tall flex row: <Navbar.Brand> and <Navbar.Items> on the left, <Navbar.Actions> on the right. active on an item sets aria-current="page". Items accept a leading icon prop.
<header class="navbar" style="--color-system-accent: var(--color-purple-600)"> <div class="navbar-brand"> <span class="brand-tile" aria-hidden>A</span> Acme </div> <nav class="navbar-items"> <a class="navbar-item" href="#" aria-current="page"> <i class="ti ti-home" aria-hidden="true"></i> Dashboard </a> <a class="navbar-item" href="#"> <i class="ti ti-receipt" aria-hidden="true"></i> Orders </a> <a class="navbar-item" href="#">Customers</a> </nav> <div class="navbar-actions"> <button class="btn btn-ghost btn-sm" type="button">Sign out</button> </div></header><Navbar style={{ "--color-system-accent": "var(--color-purple-600)" }}> <Navbar.Brand> <BrandTile monogram="A" /> Acme </Navbar.Brand> <Navbar.Items> <Navbar.Item href="#" active icon={IconHome}> Dashboard </Navbar.Item> <Navbar.Item href="#" icon={IconReceipt}> Orders </Navbar.Item> <Navbar.Item href="#">Customers</Navbar.Item> </Navbar.Items> <Navbar.Actions> <button className="btn btn-ghost btn-sm" type="button"> Sign out </button> </Navbar.Actions></Navbar>Dropdowns
Section titled “Dropdowns”<Navbar.Dropdown> is a <Menu> styled to fit the navbar.
<header class="navbar" style="--color-system-accent: var(--color-purple-600)"> <div class="navbar-brand"> <span class="brand-tile" aria-hidden>A</span> Acme </div> <nav class="navbar-items"> <a class="navbar-item" href="#">Dashboard</a> <details class="menu"> <summary class="menu-trigger navbar-item">Products</summary> <div class="menu-popup" role="menu"> <button class="menu-item" type="button">Catalogue</button> <button class="menu-item" type="button">Categories</button> <hr class="menu-separator" /> <button class="menu-item" type="button">Imports</button> </div> </details> </nav></header><Navbar style={{ "--color-system-accent": "var(--color-purple-600)" }}> <Navbar.Brand> <BrandTile monogram="A" /> Acme </Navbar.Brand> <Navbar.Items> <Navbar.Item href="#">Dashboard</Navbar.Item> <Navbar.Dropdown label="Products"> <Menu.Item>Catalogue</Menu.Item> <Menu.Item>Categories</Menu.Item> <Menu.Separator /> <Menu.Item>Imports</Menu.Item> </Navbar.Dropdown> </Navbar.Items></Navbar>Actions slot
Section titled “Actions slot”The vanilla example uses a native <select>; React’s <Select> is preferable when option rows need custom rendering (icons, two lines, etc.).
<header class="navbar" style="--color-system-accent: var(--color-green-600)"> <div class="navbar-brand"> <span class="brand-tile" aria-hidden>AO</span> AO Retail </div> <div class="navbar-actions"> <select class="select select-sm" style="width: auto" aria-label="Shop"> <option value="billigvvs.dk">BilligVVS.dk</option> <option value="lavprisvvs.dk">LavprisVVS.dk</option> <option value="elproffs.se">ELproffs.se</option> </select> <details class="menu"> <summary class="menu-trigger navbar-item">Nickolaj</summary> <div class="menu-popup" role="menu"> <button class="menu-item" type="button">Profile</button> <hr class="menu-separator" /> <button class="menu-item" type="button">Sign out</button> </div> </details> </div></header><Navbar style={{ "--color-system-accent": "var(--color-green-600)" }}> <Navbar.Brand> <BrandTile monogram="AO" /> AO Retail </Navbar.Brand> <Navbar.Actions> <Select defaultValue="billigvvs.dk"> <Select.Trigger triggerSize="sm" aria-label="Shop"> <Select.Value /> <Select.Icon /> </Select.Trigger> <Select.Popup> <Select.Item value="billigvvs.dk"> <Select.ItemText>BilligVVS.dk</Select.ItemText> </Select.Item> <Select.Item value="lavprisvvs.dk"> <Select.ItemText>LavprisVVS.dk</Select.ItemText> </Select.Item> <Select.Item value="elproffs.se"> <Select.ItemText>ELproffs.se</Select.ItemText> </Select.Item> </Select.Popup> </Select> <Navbar.Dropdown label="Nickolaj"> <Menu.Item>Profile</Menu.Item> <Menu.Separator /> <Menu.Item>Sign out</Menu.Item> </Navbar.Dropdown> </Navbar.Actions></Navbar>Mobile toggle
Section titled “Mobile toggle”<Navbar.MobileToggle> is hidden at ≥ 48rem (Tailwind md) and flips <AppShell>’s mobile drawer state — it’s a no-op outside <AppShell>. The default aria-label is "Open menu"; override via label.
<button class="navbar-mobile-toggle" type="button" aria-label="Open menu"></button><Navbar.MobileToggle />See mobile drawer below.
Sidebar
Section titled “Sidebar”Flat items, tree groups, and click-to-collapse, driven by native HTML.
Items and groups
Section titled “Items and groups”<Sidebar.Item> is a leaf link; active marks the current route, icon shows a leading glyph, badge adds a trailing count or pill. <Sidebar.Group> clusters items under an optional <Sidebar.GroupLabel> that hides when collapsed. <Sidebar.Header> is the slot for an app logo or product switcher above the nav.
<Sidebar style={{ height: "20rem" }}> <Sidebar.Nav> <Sidebar.Group> <Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel> <Sidebar.Item href="#" active icon={IconHome}> Dashboard </Sidebar.Item> <Sidebar.Item href="#" icon={IconReceipt} badge="12"> Orders </Sidebar.Item> </Sidebar.Group> <Sidebar.Group> <Sidebar.GroupLabel>Catalogue</Sidebar.GroupLabel> <Sidebar.Item href="#" icon={IconPackage}> Products </Sidebar.Item> <Sidebar.Item href="#" icon={IconChartBar}> Categories </Sidebar.Item> </Sidebar.Group> </Sidebar.Nav></Sidebar>Tree navigation
Section titled “Tree navigation”<Sidebar.Collapsible> is a <details> revealing <Sidebar.SubItem> rows. Pass defaultOpen to start expanded, or open + onOpenChange for controlled state. Override the trigger entirely with trigger.
<aside class="sidebar" style="height: 22rem"> <nav class="sidebar-nav"> <a class="sidebar-item" href="#"> <span class="sidebar-icon" ><i class="ti ti-receipt" aria-hidden="true"></i ></span> <span class="sidebar-label">Ordrer</span> </a> <details class="sidebar-collapsible" open> <summary class="sidebar-collapsible-trigger"> <span class="sidebar-icon" ><i class="ti ti-shopping-cart" aria-hidden="true"></i ></span> <span class="sidebar-label">Webshop</span> </summary> <div class="sidebar-collapsible-panel"> <a class="sidebar-subitem" href="#" aria-current="page">CMS</a> <a class="sidebar-subitem" href="#">Kampagner</a> <a class="sidebar-subitem" href="#">Søgeord</a> <a class="sidebar-subitem" href="#">Redirects</a> </div> </details> <a class="sidebar-item" href="#"> <span class="sidebar-icon" ><i class="ti ti-package" aria-hidden="true"></i ></span> <span class="sidebar-label">Lager</span> </a> </nav></aside><Sidebar style={{ height: "22rem" }}> <Sidebar.Nav> <Sidebar.Item href="#" icon={IconReceipt}> Ordrer </Sidebar.Item> <Sidebar.Collapsible defaultOpen icon={IconShoppingCart} label="Webshop"> <Sidebar.SubItem href="#" active> CMS </Sidebar.SubItem> <Sidebar.SubItem href="#">Kampagner</Sidebar.SubItem> <Sidebar.SubItem href="#">Søgeord</Sidebar.SubItem> <Sidebar.SubItem href="#">Redirects</Sidebar.SubItem> </Sidebar.Collapsible> <Sidebar.Item href="#" icon={IconPackage}> Lager </Sidebar.Item> </Sidebar.Nav></Sidebar>Click to collapse
Section titled “Click to collapse”<Sidebar.CollapseToggle> is a <label> wrapping a hidden checkbox; the rail responds to .sidebar:has(.sidebar-toggle:checked). Pass each item’s icon so it stays visible when collapsed.
React’s <Sidebar> exposes collapsed / defaultCollapsed / onCollapsedChange for controlled state.
<aside class="sidebar" style="height: 20rem"> <nav class="sidebar-nav"> <a class="sidebar-item" href="#" aria-current="page"> <span class="sidebar-icon"> <i class="ti ti-home" aria-hidden="true"></i> </span> <span class="sidebar-label">Dashboard</span> </a> <a class="sidebar-item" href="#"> <span class="sidebar-icon"> <i class="ti ti-receipt" aria-hidden="true"></i> </span> <span class="sidebar-label">Orders</span> </a> </nav> <div class="sidebar-footer"> <label class="sidebar-collapse-toggle"> <input type="checkbox" class="sidebar-toggle" /> <span class="sr-only">Toggle sidebar</span> </label> </div></aside><Sidebar style={{ height: "20rem" }}> <Sidebar.Nav> <Sidebar.Item href="#" active icon={IconHome}> Dashboard </Sidebar.Item> <Sidebar.Item href="#" icon={IconReceipt}> Orders </Sidebar.Item> </Sidebar.Nav> <Sidebar.Footer> <Sidebar.CollapseToggle /> </Sidebar.Footer></Sidebar>Mobile drawer
Section titled “Mobile drawer”Below md the desktop sidebar hides and <Navbar.MobileToggle> opens it as a drawer. Esc, backdrop click, and link clicks all dismiss; focus is trapped while open. Override the drawer’s accessible label via <Sidebar drawerLabel="...">.
<AppShell> accepts mobileDrawerOpen / defaultMobileDrawerOpen / onMobileDrawerOpenChange for controlled drawer state — useful when an external trigger (a route guard, a tutorial step) needs to open it.
<AppShell hasSidebar systemAccent="var(--color-purple-600)" style={{ minHeight: "24rem" }}> <Navbar> <Navbar.MobileToggle /> <Navbar.Brand> <BrandTile monogram="A" /> Acme </Navbar.Brand> </Navbar> <Sidebar> <Sidebar.Nav> <Sidebar.Item href="#" active icon={IconHome}> Dashboard </Sidebar.Item> <Sidebar.Item href="#" icon={IconReceipt}> Orders </Sidebar.Item> </Sidebar.Nav> </Sidebar> <AppShell.Main style={{ padding: "1rem" }}> Resize below 768px and tap the hamburger. </AppShell.Main></AppShell>Footer
Section titled “Footer”<Footer.Links> on the left, <Footer.Meta> on the right; both wrap on narrow viewports.
<footer class="footer"> <div class="footer-links"> <a class="footer-link" href="#">Docs</a> <a class="footer-link" href="#">Status</a> <a class="footer-link" href="#">Support</a> </div> <div class="footer-meta">v1.4.0 · © Acme</div></footer><Footer> <Footer.Links> <Footer.Link href="#">Docs</Footer.Link> <Footer.Link href="#">Status</Footer.Link> <Footer.Link href="#">Support</Footer.Link> </Footer.Links> <Footer.Meta>v1.4.0 · © Acme</Footer.Meta></Footer>Examples
Section titled “Examples”Classic admin
Section titled “Classic admin”<AppShell hasSidebar systemAccent="var(--color-green-600)" style={{ minHeight: "32rem" }}> <Navbar> <Navbar.MobileToggle /> <Navbar.Brand> <BrandTile monogram="AO" /> AO Retail </Navbar.Brand> <Navbar.Actions> <Select defaultValue="billigvvs.dk"> <Select.Trigger triggerSize="sm" aria-label="Shop"> <Select.Value /> <Select.Icon /> </Select.Trigger> <Select.Popup> <Select.Item value="billigvvs.dk"> <Select.ItemText>BilligVVS.dk</Select.ItemText> </Select.Item> <Select.Item value="lavprisvvs.dk"> <Select.ItemText>LavprisVVS.dk</Select.ItemText> </Select.Item> <Select.Item value="elproffs.se"> <Select.ItemText>ELproffs.se</Select.ItemText> </Select.Item> <Select.Item value="vvskupp.no"> <Select.ItemText>VVSkupp.no</Select.ItemText> </Select.Item> </Select.Popup> </Select> <Navbar.Dropdown label="Nickolaj"> <Menu.Item>Profile</Menu.Item> <Menu.Separator /> <Menu.Item>Sign out</Menu.Item> </Navbar.Dropdown> </Navbar.Actions> </Navbar> <Sidebar> <Sidebar.Nav> <Sidebar.Item href="#" icon={IconSettings}> Indstillinger </Sidebar.Item> <Sidebar.Item href="#" icon={IconReceipt}> Ordrer </Sidebar.Item> <Sidebar.Item href="#" icon={IconHeadset}> Kundeservice </Sidebar.Item> <Sidebar.Item href="#" icon={IconPackage}> Produkter </Sidebar.Item> <Sidebar.Collapsible defaultOpen icon={IconShoppingCart} label="Webshop" > <Sidebar.SubItem href="#" active> CMS </Sidebar.SubItem> <Sidebar.SubItem href="#">Kampagner</Sidebar.SubItem> <Sidebar.SubItem href="#">Søgeord</Sidebar.SubItem> <Sidebar.SubItem href="#">Redirects</Sidebar.SubItem> </Sidebar.Collapsible> <Sidebar.Item href="#" icon={IconTruck}> Lager </Sidebar.Item> <Sidebar.Item href="#" icon={IconChartBar}> Statistik </Sidebar.Item> </Sidebar.Nav> <Sidebar.Footer> <Sidebar.CollapseToggle /> </Sidebar.Footer> </Sidebar> <AppShell.Main style={{ padding: "1rem" }}>Page content</AppShell.Main> <Footer> <Footer.Links> <Footer.Link href="#">Docs</Footer.Link> <Footer.Link href="#">Status</Footer.Link> </Footer.Links> <Footer.Meta>© AO Retail</Footer.Meta> </Footer></AppShell>Top-nav heavy
Section titled “Top-nav heavy”No sidebar — primary navigation in the navbar via <Navbar.Dropdown>. For tools with few top-level destinations and per-destination tabs in main.
<AppShell systemAccent="var(--color-orange-600)" style={{ minHeight: "28rem" }}> <Navbar> <Navbar.Brand> <BrandTile icon={IconChartBar} /> Insights </Navbar.Brand> <Navbar.Items> <Navbar.Item href="#" active> Dashboard </Navbar.Item> <Navbar.Dropdown label="Reports"> <Menu.Item>Sales</Menu.Item> <Menu.Item>Returns</Menu.Item> <Menu.Item>Inventory</Menu.Item> <Menu.Separator /> <Menu.Item>Custom…</Menu.Item> </Navbar.Dropdown> <Navbar.Dropdown label="Customers"> <Menu.Item>Segments</Menu.Item> <Menu.Item>Lifetime value</Menu.Item> </Navbar.Dropdown> <Navbar.Item href="#">Settings</Navbar.Item> </Navbar.Items> <Navbar.Actions> <Navbar.Dropdown label="Nickolaj"> <Menu.Item>Profile</Menu.Item> <Menu.Separator /> <Menu.Item>Sign out</Menu.Item> </Navbar.Dropdown> </Navbar.Actions> </Navbar> <AppShell.Main style={{ padding: "1rem" }}>Page content</AppShell.Main> <Footer> <Footer.Links> <Footer.Link href="#">Docs</Footer.Link> <Footer.Link href="#">Changelog</Footer.Link> <Footer.Link href="#">Support</Footer.Link> </Footer.Links> <Footer.Meta>v2.1.0</Footer.Meta> </Footer></AppShell>Dashboard
Section titled “Dashboard”<Container> goes inside <AppShell.Main>, which has no padding of its own, and stacks the page sections. For the search-and-filter row that usually sits above the table, see Tables › Filter toolbar.
<AppShell hasSidebar systemAccent="var(--color-blue-600)" style={{ minHeight: "30rem" }}> <Navbar> <Navbar.Brand> <BrandTile monogram="A" /> Acme Admin </Navbar.Brand> </Navbar> <Sidebar> <Sidebar.Nav> <Sidebar.Item href="#" active icon={IconHome}> Dashboard </Sidebar.Item> <Sidebar.Item href="#" icon={IconReceipt}> Orders </Sidebar.Item> </Sidebar.Nav> </Sidebar> <AppShell.Main> <Container> <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> <StatCard icon={IconShoppingCart} label="Orders today" value="128" detail="14 awaiting fulfilment" /> <StatCard icon={IconCash} label="Revenue" value="$8.4k" detail="+8% vs target" /> <StatCard label="Failed jobs" value="3" detail="last 24h" /> </div> <Table striped> <Table.Head> <Table.Row> <Table.HeaderCell>Order</Table.HeaderCell> <Table.HeaderCell>Customer</Table.HeaderCell> <Table.HeaderCell>Status</Table.HeaderCell> </Table.Row> </Table.Head> <Table.Body> <Table.Row> <Table.Cell>#1001</Table.Cell> <Table.Cell>Ada Lovelace</Table.Cell> <Table.Cell>Shipped</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>#1002</Table.Cell> <Table.Cell>Grace Hopper</Table.Cell> <Table.Cell>Processing</Table.Cell> </Table.Row> <Table.Row> <Table.Cell>#1003</Table.Cell> <Table.Cell>Alan Turing</Table.Cell> <Table.Cell>Shipped</Table.Cell> </Table.Row> </Table.Body> </Table> </Container> </AppShell.Main> <Footer> <Footer.Meta>© Acme</Footer.Meta> </Footer></AppShell>Customization
Section titled “Customization”Two CSS variables on .app-shell set the rail width:
| Variable | Default | What it controls |
|---|---|---|
--app-shell-sidebar-w |
240px |
Expanded sidebar / drawer width. |
--app-shell-sidebar-w-collapsed |
56px |
Width of the icon rail when collapsed. |
.app-shell { --app-shell-sidebar-w: 280px;}Branding multiple systems
Section titled “Branding multiple systems”The navbar renders a 2px bottom stripe driven by --color-system-accent; the footer mirrors it with a matching top stripe. Setting that variable app-wide retints both. To tag several systems in one app, set it per shell instead — <AppShell systemAccent> in React, an inline style in vanilla — and drop a <BrandTile> into <Navbar.Brand>:
<header class="navbar" style="--color-system-accent: var(--color-purple-600)"> <div class="navbar-brand"> <span class="brand-tile" aria-hidden>OR</span> Orders </div></header><Navbar style={{ "--color-system-accent": "var(--color-purple-600)" }}> <Navbar.Brand> <BrandTile monogram="OR" /> Orders </Navbar.Brand></Navbar>For the derived tokens (-hover, -muted, -content) and the bright-accent contrast caveat, see Theming › System accent.
Reference
Section titled “Reference”Four independent compounds. <AppShell> supplies only the grid and the mobile-drawer wiring — <Navbar>, <Sidebar> and <Footer> each work standalone.
| Part | Renders | Class |
|---|---|---|
AppShell |
<div> |
app-shell |
AppShell.Main |
<main> |
app-shell-main |
Navbar |
<header> |
navbar |
Navbar.Brand |
<div> |
navbar-brand |
Navbar.Items |
<nav> |
navbar-items |
Navbar.Item |
<a> |
navbar-item |
Navbar.Dropdown |
<details> (Menu) |
menu, with navbar-item on the trigger |
Navbar.Actions |
<div> |
navbar-actions |
Navbar.MobileToggle |
<button> |
navbar-mobile-toggle |
Sidebar |
<aside> |
sidebar |
Sidebar.Header |
<div> |
sidebar-header |
Sidebar.Nav |
<nav> |
sidebar-nav |
Sidebar.Group |
<div> |
sidebar-group |
Sidebar.GroupLabel |
<div> |
sidebar-group-label |
Sidebar.Item |
<a> |
sidebar-item |
Sidebar.Icon |
<span> |
sidebar-icon |
Sidebar.Label |
<span> |
sidebar-label |
Sidebar.Badge |
<span> |
sidebar-badge |
Sidebar.Collapsible |
<details> |
sidebar-collapsible |
Sidebar.SubItem |
<a> |
sidebar-subitem |
Sidebar.Footer |
<div> |
sidebar-footer |
Sidebar.CollapseToggle |
<label> |
sidebar-collapse-toggle, sidebar-toggle |
Footer |
<footer> |
footer |
Footer.Links |
<div> |
footer-links |
Footer.Link |
<a> |
footer-link |
Footer.Meta |
<div> |
footer-meta |
| Part | Prop | Type | Default |
|---|---|---|---|
AppShell |
hasSidebar |
boolean |
false |
AppShell |
systemAccent |
string (CSS color) |
inherited |
AppShell |
mobileDrawerOpen |
boolean |
uncontrolled |
AppShell |
defaultMobileDrawerOpen |
boolean |
false |
AppShell |
onMobileDrawerOpenChange |
(open: boolean) => void |
— |
Navbar.Item |
active |
boolean |
false |
Navbar.Item |
icon |
component or element | — |
Navbar.Dropdown |
label |
ReactNode |
required |
Navbar.MobileToggle |
label |
string |
"Open menu" |
Sidebar |
collapsed |
boolean |
uncontrolled |
Sidebar |
defaultCollapsed |
boolean |
false |
Sidebar |
onCollapsedChange |
(collapsed: boolean) => void |
— |
Sidebar |
drawerLabel |
string |
"Navigation" |
Sidebar.Item |
active |
boolean |
false |
Sidebar.Item |
icon / badge |
component or element | — |
Sidebar.Collapsible |
icon / label |
component or element | — |
Sidebar.Collapsible |
trigger |
ReactNode |
icon + label |
Sidebar.Collapsible |
open / defaultOpen |
boolean |
uncontrolled |
Sidebar.Collapsible |
onOpenChange |
(open: boolean) => void |
— |
Sidebar.SubItem |
active / icon / badge |
as Sidebar.Item |
— |
Sidebar.CollapseToggle |
label |
string |
"Toggle sidebar" |
active writes aria-current="page". Each part also takes the native attributes of its element, and Sidebar, Sidebar.Item, Sidebar.SubItem, Sidebar.Collapsible and Sidebar.CollapseToggle take classNames.
<Navbar.MobileToggle> and <Sidebar>’s drawer both read <AppShell>’s context, so the toggle is inert outside a shell. When the drawer opens, <Sidebar> moves its children into the drawer rather than duplicating them — state held in a sidebar child does not survive crossing that breakpoint.
Vanilla
Section titled “Vanilla”| Class | Effect |
|---|---|
app-shell |
min-height: 100vh grid, rows auto 1fr auto as areas header / main / footer |
app-shell-with-sidebar |
Adds a leading column sized to the sidebar’s own width; below 48rem drops back to one column |
app-shell-main |
Claims the main area with min-width: 0, so a wide table scrolls instead of stretching the grid |
navbar |
3rem flex row, 1rem side padding, muted surface, 2px bottom stripe in --color-system-accent |
navbar-brand |
Semibold text-sm row that never shrinks |
navbar-items |
Flex row, 0.125rem gap |
navbar-item |
text-sm pill, fills on hover; aria-current="page" or [data-active] switches it to primary-on-muted |
navbar-actions |
Flex row pushed right with margin-left: auto |
navbar-mobile-toggle |
2.25rem square hamburger drawn from one ::before bar plus two box-shadow strokes; hidden at ≥ 48rem |
sidebar |
Column flex rail at --app-shell-sidebar-w (240px), muted surface, right border, 150ms width transition |
sidebar-toggle |
1px visually-hidden checkbox — .sidebar:has(.sidebar-toggle:checked) is what drives the collapsed state |
sidebar-header |
3rem row above the nav with a bottom border, for a logo or product switcher |
sidebar-nav |
Scrolling column that fills the remaining height, 0.125rem gap |
sidebar-group |
Column of items; a following group gets 0.5rem of top margin |
sidebar-group-label |
text-xs uppercase muted heading; hidden when collapsed |
sidebar-item |
text-sm row with a 0.5rem gap; active state adds a primary-muted fill and medium weight |
sidebar-icon |
1rem muted glyph box — the one part that stays visible in the collapsed rail; turns primary when active |
sidebar-label |
Truncating text that fills the row; hidden when collapsed |
sidebar-badge |
1.25rem pill pushed to the row’s trailing edge; hidden when collapsed |
sidebar-collapsible |
<details> wrapper; height animates via ::details-content and interpolate-size: allow-keywords |
sidebar-collapsible-trigger |
<summary> styled as an item, native marker removed, CSS chevron rotating 45° when open |
sidebar-collapsible-panel |
Sub-item column indented 1rem; hidden when collapsed |
sidebar-subitem |
Like sidebar-item with tighter vertical padding and no icon box |
sidebar-footer |
Bottom slot with a top border |
sidebar-collapse-toggle |
1.75rem square <label> whose CSS chevron flips direction when the inner sidebar-toggle is checked |
sidebar-drawer |
Fixed leading panel at min(--app-shell-sidebar-w, 80vw), sliding in from fully off-screen |
sidebar-drawer-backdrop |
Fixed scrim over the page that fades in with the drawer |
footer |
Wrapping space-between row, text-xs muted, 2px top stripe in --color-system-accent |
footer-links |
Wrapping row, 0.75rem gap |
footer-link |
Muted, no underline; brightens and underlines on hover |
footer-meta |
Muted text block |
The grid areas are assigned by child class — .app-shell > .navbar, > .sidebar, > main, > .footer — so all four must be direct children. Wrapping one in a <div> drops it out of its area. app-shell-main exists for markup that can’t use a bare <main>; .app-shell > main already claims the area. Rail widths come from two custom properties, see Customization.
The collapsed rail needs no JavaScript: check the hidden sidebar-toggle and :has() does the rest. Write aria-current="page" yourself.
The mobile drawer is the one piece with no vanilla equivalent — sidebar-drawer and sidebar-drawer-backdrop are styled for Base UI’s [data-starting-style] / [data-ending-style] transition hooks. In vanilla, either leave the sidebar visible at every width or build the drawer from a <dialog class="drawer"> and toggle it yourself.