Skip to content

App shell

A CSS grid with named areas — header, sidebar, main, footer — plus a small React context for the mobile drawer. The composed pieces (navbar, sidebar, footer) also work standalone. The shell doesn’t assume it owns the viewport.

+--------------------------------------------+
| navbar |
+----------+---------------------------------+
| | |
| sidebar | main |
| (opt.) | |
| | |
+----------+---------------------------------+
| footer (opt.) |
+--------------------------------------------+

Navbar above, main below.

Page content
<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>

Add hasSidebar for a two-column grid, hasFooter for a bottom row.

<AppShell
hasSidebar
hasFooter
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>

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".

<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">Dashboard</a>
<a class="navbar-item" href="#">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.Dropdown> is a <Menu> styled as a navbar item — menu-trigger plus navbar-item on the <summary>.

<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>

Right-aligned slot for shop switcher, user menu, sign-out, etc. 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-bordered 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.MobileToggle> is hidden above the md breakpoint and flips AppShell’s mobile drawer state.

<button class="navbar-mobile-toggle" type="button" aria-label="Open menu"></button>

See mobile drawer below.

Flat items, tree groups, and click-to-collapse — all driven by native HTML.

<Sidebar.Item> is a leaf link; active marks the current route. <Sidebar.Group> clusters items under an optional <Sidebar.GroupLabel> that hides when collapsed.

<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}>
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>

<Sidebar.Collapsible> is a <details> revealing <Sidebar.SubItem> rows. Pass defaultOpen to start expanded.

<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.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>

Inside <AppShell>, the sidebar registers a Dialog drawer. Below md, the desktop column hides and <Navbar.MobileToggle> opens the drawer. Esc, backdrop click, and link clicks all close it; focus is trapped while open.

Resize below 768px and tap the hamburger.
<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.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>

Full compositions to copy and adapt.

Sidebar-first with a shop selector in the navbar, tree navigation, and a collapse toggle.

<AppShell
hasSidebar
hasFooter
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>

No sidebar — primary navigation in the navbar via <Navbar.Dropdown>. Good for tools with few top-level destinations and per-destination tabs in main.

<AppShell
hasFooter
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>

Two CSS variables on .app-shell set the rail width:

VariableDefaultWhat it controls
--app-shell-sidebar-w240pxExpanded sidebar / drawer width.
--app-shell-sidebar-w-collapsed56pxWidth of the icon rail when collapsed.
.app-shell {
--app-shell-sidebar-w: 280px;
}

The navbar always renders a 2px bottom stripe driven by --color-system-accent; the footer mirrors it with a matching top stripe. Drop a <BrandTile> into <Navbar.Brand> and override the variable to tag a system — either app-wide at :root, or per-shell via <AppShell systemAccent>:

:root {
--color-system-accent: var(--color-purple-600);
}

The default is a neutral gray, so un-branded apps read as chrome.

<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>

For derived tokens (-hover, -muted, -content) and the yellow-accent contrast caveat, see Customize › System accent.