App Shell

App frame with a collapsible sidebar and an inset content panel.

Installation

pnpm dlx shadcn@latest add @paneui/app-shell

Usage

Composition

Examples

Header First

Sidebar

Floating

Top Nav

Icon Rail

Header Actions

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.

The sidebar parts come from Animated Sidebar.

API Reference

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"
page fills the viewport and sticks to it; window is a rounded frame whose panel scrolls inside.
layout= "inset"
"inset" | "sidebar" | "floating"
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= 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. null disables it.
PropTypeDefaultDescription
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.
openboolean—Controlled expanded state of the rail.
defaultOpenbooleantrueInitial expanded state when uncontrolled.
onOpenChange(open: boolean) => void—Called when the rail expands or collapses.
shortcutstring | null"b"Key that toggles the rail with ⌘/Ctrl. null disables it.
data-state
expanded or collapsed.
data-variant
page or window.
data-layout
inset, sidebar or floating.
Data attributeDescription
data-stateexpanded or collapsed.
data-variantpage or window.
data-layoutinset, 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. null leaves it empty.
PropTypeDefaultDescription
leadingReactNode<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
expanded or collapsed.
Data attributeDescription
data-stateexpanded 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.
PropTypeDefaultDescription
mobileTitleReactNode"Navigation"Title of the mobile drawer.
aria-labelstring"Sidebar"Accessible name of the sidebar and the drawer's sidebar.

AppShellMain

The inset content panel. Bring your own padding. Also accepts every prop of main.

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 AppShellTrigger and a vertical divider.
PropTypeDefaultDescription
triggerbooleantrueLead 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.
PropTypeDefaultDescription
name*ReactNode—The current team.
descriptionReactNode—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.
childrenReactNode—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.
PropTypeDefaultDescription
variant"page" | "window"—The shell variant.
isMobileboolean—Whether the viewport is below lg (the sidebar is a drawer).
mobileOpenboolean—Whether the drawer is open.
setMobileOpen(open: boolean) => void—Open or close the drawer.