Components
Sidebar
One component, two presentations: a modal overlay drawer (the default), and a persistent docked panel that collapses to an icon rail and degrades to the drawer on small screens.
Overlay drawer
The default variant slides in over the page with a blurred backdrop. It closes on backdrop click or Escape and locks body scroll while open.
import { AppShell, Header, Content, Sidebar, NavGroup, NavItem } from "appshell-react";
import { useState } from "react";
import { Menu, Home, Package, Settings } from "lucide-react";
export default function App() {
const [open, setOpen] = useState(false);
return (
<AppShell safeArea>
<Header
behavior="fixed"
logo={
<button aria-label="Open menu" onClick={() => setOpen(true)}>
<Menu />
</button>
}
/>
<Sidebar open={open} onClose={() => setOpen(false)} side="left">
<NavGroup title="Navigation" defaultOpen>
<NavItem label="Home" icon={<Home />} active />
<NavItem label="Products" icon={<Package />} />
<NavItem label="Settings" icon={<Settings />} />
</NavGroup>
</Sidebar>
<Content>{/* main content */}</Content>
</AppShell>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "overlay" | "overlay" | Selects the overlay presentation — the default when omitted. |
| open* | boolean | — | Whether the drawer is visible. |
| onClose* | () => void | — | Called on backdrop click or Escape. |
| side | "start" | "end" | "left" | "right" | "start" | Which edge the drawer slides from. "start"/"end" follow the writing direction from I18nProvider (start is left in LTR, right in RTL); "left"/"right" are physical and never flip. |
| aria-label | string | — | Accessible name of the drawer. Defaults to the "Navigation menu" label. |
| topContent | ReactNode | — | Pinned above the scrolling nav — a SearchField, a workspace switcher. The drawer panel itself clears the top safe area, slot or not. |
| bottomContent | ReactNode | — | Pinned below the scrolling nav — settings, about, a theme toggle, a UserMenu. |
| className | string | — | Extra classes for the panel. |
| children* | ReactNode | — | Drawer content — typically NavGroups and NavItems. |
Docked panel
variant="docked" turns the sidebar into a layout column: it sticks below the Header (via the --header-height variable), scrolls independently, and — with collapsible — collapses to an icon rail where NavItem labels hide and become tooltips. Below the breakpoint the panel disappears and the same children open as the overlay drawer through open/onClose.
import { AppShell, Header, Content, Sidebar, NavGroup, NavItem } from "appshell-react";
import { useState } from "react";
export default function App() {
const [open, setOpen] = useState(false); // mobile drawer fallback
return (
<AppShell>
<Header behavior="fixed" logo={<span className="font-bold">Console</span>} />
{/* Docked on md+, drawer below md. AppShell builds the two-column
layout automatically for direct docked-Sidebar children. */}
<Sidebar
variant="docked"
breakpoint="md"
collapsible
open={open}
onClose={() => setOpen(false)}
>
<NavGroup title="Workspace" defaultOpen>
<NavItem label="Dashboard" active />
<NavItem label="Reports" />
<NavItem label="Members" badge={2} />
</NavGroup>
</Sidebar>
<Content className="p-6">{/* main content */}</Content>
</AppShell>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
| variant* | "docked" | — | Selects the docked presentation. |
| breakpoint | "sm" | "md" | "lg" | "none" | "md" | Below this viewport width the panel hides and open/onClose drive the overlay drawer instead. "none" keeps it docked everywhere and renders no drawer. |
| collapsible | boolean | false | Render the built-in collapse-to-rail toggle. |
| collapsed | boolean | — | Controlled collapse state. |
| defaultCollapsed | boolean | false | Uncontrolled initial collapse state. |
| onCollapsedChange | (collapsed: boolean) => void | — | Fires when the toggle flips the collapse state. |
| width | string | "16rem" | Expanded panel width. |
| railWidth | string | "3.25rem" | Collapsed rail width. |
| open | boolean | false | Drives the mobile drawer fallback below the breakpoint. |
| onClose | () => void | — | Closes the mobile drawer fallback. |
| side | "start" | "end" | "left" | "right" | "start" | Which side of the content the panel docks to. "start"/"end" follow the writing direction from I18nProvider (start is left in LTR, right in RTL); "left"/"right" are physical and never flip. |
| aria-label | string | — | Accessible name of the panel (and its drawer fallback). Defaults to the "Navigation menu" label. |
| topContent | ReactNode | — | Pinned above the scrolling nav in both the docked panel and the drawer fallback — the desktop home for a SearchField, filling the space a docked panel usually leaves empty. |
| bottomContent | ReactNode | — | Pinned below the scrolling nav in both the docked panel and the drawer fallback — settings, about, a theme toggle, a UserMenu. |
| className | string | — | Extra classes for the panel. |
variant="docked" Sidebar — resize the window to watch it fold into the drawer. Two v1 limitations to know: fixed Footers and the Header’s reveal overlay span the full viewport width (they do not stop at the rail), and docked children render in both the panel and the drawer, so avoid hard-coded element ids inside.Top and bottom sections
Both variants take a topContent and a bottomContent prop — slots pinned above and below the scrolling nav. The top slot is the desktop home for a SearchField (pass inset={false} and pad the slot yourself) or a workspace switcher: with a docked panel the header stays clean, and the space the sidebar usually leaves empty earns its keep. The bottom slot holds the infrastructural actions that don’t navigate — settings, about, a theme toggle, or a UserMenu. In the drawer, the panel itself clears the status bar — slot or no slot — and the bottom edge clears the home indicator, whether the inset lands on the bottom slot or on the scrolling nav. The pattern comes straight from industrial design systems: navigation scrolls, infrastructure stays put.
Keyboard support
- •
Escapecloses the drawer presentation (overlay variant, or the docked variant’s mobile fallback) - • The collapse toggle is a native button:
Enter/Spaceactivate it, witharia-expandedand a visible focus ring - • The open drawer traps
Tab/Shift+Tabinside itself, pulls focus in on the first Tab, and hands focus back to the trigger on close
Overlay stack
The open overlay drawer shares its Escape handling, scroll lock, and z-index stacking with every other overlay in the library (SearchModal, a modal BottomSheet, UserMenu/NotificationsMenu) — one stack, so opening a menu inside the drawer closes just the menu, not the drawer underneath it. See BottomSheet’s overlay stack section for the full mechanism, and the nested overlays example to see it live.