App Shell
App frame with a collapsible sidebar and an inset content panel.
Installation
pnpm dlx shadcn@latest add @paneui/app-shellUsage
Composition
Examples
Sidebar first or header first
Without a direct AppShellHeader child the sidebar runs the full height and the panel sits 3px below the top, with AppShellInsetHeader as its first row. With one, the header spans the top and the sidebar and panel start below it. The choice is made in CSS from the children, so it renders right on the server.
Alignment
The header's leading column is exactly --sidebar-width-icon wide, so the trigger sits on the rail's icon column whether the sidebar is expanded or folded. Header content starts on the sidebar's label column (--app-shell-label-inset), so a title or logo lines up with the nav labels. Change the rail width on AppShell with style={{ "--sidebar-width-icon": "4rem" }} and both follow. The floating rail grows by its 3px gutter and the trigger moves with it, so the icons stay on the trigger's column. AppShellSwitcher puts a 32px avatar on the same column.
Page and window
variant="page" fills the viewport: the header and the rail stick, the panel scrolls with the page and a sticky cap keeps its rounded top under the header, so start your content at least 1rem below the panel's top. variant="window" is a bounded frame (give it a height) whose panel scrolls on its own; the panel's radius is the frame's radius minus its 3px gap.
Mobile
Below the lg breakpoint the sidebar's children move into a left drawer and AppShellTrigger opens it. Following a link inside the drawer closes it; disclosure rows stay open. Lead mobileTitle with an 18px mark so it lines up with the nav icons.
Keyboard
⌘/Ctrl + B toggles the rail; change the key with shortcut or pass null to turn it off. The trigger sets aria-expanded for the rail on desktop and for the drawer on mobile.
Related
The sidebar parts come from Animated Sidebar.
AppShell
The band. Holds the sidebar state and lays out the header, sidebar and panel on one grid. Also accepts every prop of div.
- variant= "page"
- "page" | "window"
pagefills the viewport and sticks to it;windowis a rounded frame whose panel scrolls inside.- layout= "inset"
- "inset" | "sidebar" | "floating"
insetputs the page in a rounded panel on the band;sidebaris flush, with hairlines under the header and beside the rail;floatingmakes the rail a rounded panel of its own.- open
- boolean
- Controlled expanded state of the rail.
- defaultOpen= true
- boolean
- Initial expanded state when uncontrolled.
- onOpenChange
- (open: boolean) => void
- Called when the rail expands or collapses.
- shortcut= "b"
- string | null
- Key that toggles the rail with ⌘/Ctrl.
nulldisables it.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "page" | "window" | "page" | page fills the viewport and sticks to it; window is a rounded frame whose panel scrolls inside. |
| layout | "inset" | "sidebar" | "floating" | "inset" | inset puts the page in a rounded panel on the band; sidebar is flush, with hairlines under the header and beside the rail; floating makes the rail a rounded panel of its own. |
| open | boolean | — | Controlled expanded state of the rail. |
| defaultOpen | boolean | true | Initial expanded state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called when the rail expands or collapses. |
| shortcut | string | null | "b" | Key that toggles the rail with ⌘/Ctrl. null disables it. |
- data-state
expandedorcollapsed.- data-variant
pageorwindow.- data-layout
inset,sidebarorfloating.
| Data attribute | Description |
|---|---|
| data-state | expanded or collapsed. |
| data-variant | page or window. |
| data-layout | inset, sidebar or floating. |
AppShellHeader
The top row. Children start on the sidebar's label column. Also accepts every prop of header.
- leading= <AppShellTrigger />
- ReactNode
- Content of the rail-wide leading column.
nullleaves it empty.
| Prop | Type | Default | Description |
|---|---|---|---|
| leading | ReactNode | <AppShellTrigger /> | Content of the rail-wide leading column. null leaves it empty. |
AppShellTrigger
Toggles the rail on desktop and opens the drawer below lg. Also accepts every prop of button.
- data-state
expandedorcollapsed.
| Data attribute | Description |
|---|---|
| data-state | expanded or collapsed. |
AppShellSidebar
The rail sidebar on desktop, a left drawer below lg. Takes the animated sidebar's parts as children. Also accepts every prop of Sidebar.
- mobileTitle= "Navigation"
- ReactNode
- Title of the mobile drawer.
- aria-label= "Sidebar"
- string
- Accessible name of the sidebar and the drawer's sidebar.
| Prop | Type | Default | Description |
|---|---|---|---|
| mobileTitle | ReactNode | "Navigation" | Title of the mobile drawer. |
| aria-label | string | "Sidebar" | Accessible name of the sidebar and the drawer's sidebar. |
AppShellInsetHeader
The panel's first row in a sidebar-first shell. Sticks to the panel's top. Also accepts every prop of header.
- trigger= true
- boolean
- Lead with
AppShellTriggerand a vertical divider.
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger | boolean | true | Lead with AppShellTrigger and a vertical divider. |
AppShellSwitcher
The team or workspace at the top of the sidebar, opening a menu of the others. Also accepts every prop of button.
- name*
- ReactNode
- The current team.
- description
- ReactNode
- Second line, such as the plan.
- avatar*
- ReactNode
- A 32px
Avatar shape="rounded"; it stays visible in the icon rail. - side= "bottom"
- "top" | "bottom" | "left" | "right"
- Menu side.
- align= "start"
- "start" | "center" | "end"
- Menu alignment.
- children
- ReactNode
- Menu contents.
| Prop | Type | Default | Description |
|---|---|---|---|
| name* | ReactNode | — | The current team. |
| description | ReactNode | — | Second line, such as the plan. |
| avatar* | ReactNode | — | A 32px Avatar shape="rounded"; it stays visible in the icon rail. |
| side | "top" | "bottom" | "left" | "right" | "bottom" | Menu side. |
| align | "start" | "center" | "end" | "start" | Menu alignment. |
| children | ReactNode | — | Menu contents. |
useAppShell
Read the shell state inside AppShell. Also returns everything useSidebar does.
- variant
- "page" | "window"
- The shell variant.
- isMobile
- boolean
- Whether the viewport is below
lg(the sidebar is a drawer). - mobileOpen
- boolean
- Whether the drawer is open.
- setMobileOpen
- (open: boolean) => void
- Open or close the drawer.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "page" | "window" | — | The shell variant. |
| isMobile | boolean | — | Whether the viewport is below lg (the sidebar is a drawer). |
| mobileOpen | boolean | — | Whether the drawer is open. |
| setMobileOpen | (open: boolean) => void | — | Open or close the drawer. |