App Shell
AppShell is the frame of an app: a sidebar that collapses to icons or opens as a Drawer, a header and the main content.
breakpoint up (a container query, so it follows the frame, not the screen) the sidebar sits in the layout and SidebarTrigger collapses it to an icon rail, with item names in Tooltips. Below it, the same content opens in a Drawer: following a link or choosing a Nav item closes it and focus returns to the button that opened it. Ctrl+B (⌘B) works like SidebarTrigger, except while typing in a field or composing with an IME; on a page with several AppShells (like this one) only the frame that has focus or was clicked last responds. persistKey saves the collapsed state (and width) in a cookie; the Drawer is never saved. With server rendering, read the cookie with readAppShellCookie(cookieHeader, key) and pass defaultCollapsed so nothing flashes. AppShell is a container named app-shell: parts that follow the frame width use @3xl/app-shell:…. The top bar height is the --app-shell-header variable (3.5rem by default); SidebarHeader and the sticky parts follow it. SidebarTrigger has a Tooltip with its label and shortcut. The demos below sit inside the docs page, so AppShellMain uses asChild with a div (a page has one <main>).Import
Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.
// npm install yugen-uiimport { AppShell, AppShellHeader, AppShellMain, AppShellAside, AppShellFooter, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarTrigger, SidebarRail, useAppShell, readAppShellCookie,} from "yugen-ui";Variants
Each tile is a standalone example: hover to copy, or press Code to see the source.
Narrow frame
Below the breakpoint, SidebarTrigger opens the sidebar in a Drawer. Choosing an item closes it.
42rem or wider: the sidebar is part of the layout.
Changing the breakpoint
breakpoint="2xl" (42rem). The breakpoint is measured against the AppShell's own width, not the screen: drag the frame's bottom corner to try it. Crossing it while the Drawer is open closes the Drawer and moves focus to SidebarTrigger, or to the content when the trigger is hidden; the collapsed state is kept. For your own parts that should switch at the same point, use @2xl/app-shell:…, or useAppShell().narrow for behavior (always false during server rendering). md: and useMediaQuery follow the screen, so they won't line up.
Sidebar + aside
AppShellAside sits after the content and shows from the breakpoint up. On narrow frames, move its content to a Sheet or its own page.
Content page: the whole page scrolls
No Sidebar, so its column is 0 wide. scroll="page" suits content sites (blog, store, landing): the whole page scrolls, the top bar sticks and AppShellFooter sits at the end of the page. Inside an area that scrolls itself, like this frame, that area takes the place of the window.
Top bar across
layout="header": the top bar (logo, search, account) spans the full width and the sidebar sits below it. Suits admin pages.
Hidden sidebar & useAppShell
collapsible="offcanvas" suits lists without icons (conversations, files): collapsing hides it completely. A button in the sidebar that is not a link closes the Drawer itself with useAppShell().
Usage
- Put AppShell at the root of the app, as tall as the screen; content scrolls inside AppShellMain.
- Give every NavItem in the sidebar an icon when using collapsible="icon", so the rail stays usable.
- Use persistKey so people find the sidebar as they left it when they come back.
- Don't put anything other than AppShell parts directly inside AppShell: the grid will place it in an empty cell.
- Don't nest AppShellMain (a <main>) in a page that already has one; use asChild with a div.
- Don't change Ctrl+B to a key the browser or an editor already uses.
Props
| Prop · Type | Default | Description |
|---|---|---|
| breakpoint"lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | Default: "4xl" | AppShell width (not the screen width) from which the sidebar sits in the layout and AppShellAside shows: 32 / 36 / 42 / 48 / 56 / 64rem, the same sizes as the @lg … @5xl container queries. Below it, a Drawer. |
| layout"sidebar" | "header" | Default: "sidebar" | sidebar: the sidebar runs the full height with the top bar beside it. header: the top bar spans the full width with the sidebar below it. |
| scroll"main" | "page" | Default: "main" | main: the frame is as tall as the screen and AppShellMain scrolls (apps). page: the whole page scrolls, the top bar and sidebar stick (content sites). |
| collapsed / defaultCollapsedboolean | Default: false | Sidebar collapsed on wide frames. |
| onCollapsedChange(collapsed: boolean) => void | Default: — | Called when the sidebar collapses or expands. |
| persistKeystring | Default: — | Cookie name for the collapsed state and width; a saved value wins over defaultCollapsed, and a missing or malformed cookie falls back to it. Letters, digits, _ and - only: with any other name nothing is saved (a warning in development). The cookie holds only collapsed or expanded, plus the width in px once a resizable Sidebar has been dragged. It is set on path=/ with samesite=lax, and secure on https, so each app on the same domain needs its own name. |
| shortcutstring | false | Default: "b" | Key used with Ctrl / ⌘; false turns it off. With several AppShells on a page, only the one that has focus or was clicked last responds. |
| skipLinkboolean | Default: true | A “Skip to content” link shown on focus. |
| Sidebar | Default: "icon" | Collapse to an icon rail, hide completely, or always stay open. |
| Sidebar | Default: false | SidebarRail becomes a splitter that resizes the sidebar (arrow keys, Home / End, Enter collapses). |
| Sidebar | Default: 256 / 200 / 400 | Open width in px. |
| Sidebar | Default: — | Name of the Drawer on narrow frames. |
| SidebarGroup | Default: — | Group name; in the rail it is kept for screen readers only. |
| AppShellHeader / AppShellMain / AppShellAside / AppShellFooter | Default: — | Render the header / main / aside / footer as the child element. |
| useAppShell | Default: — | narrow, collapsed, setCollapsed, drawerOpen, setDrawerOpen, toggleSidebar, rail. |
| readAppShellCookie | Default: — | Reads the saved state from document.cookie, or from the Cookie header when rendering on the server. Callable in a Server Component when imported from yugen-ui (the package root). |
Messages
Text built into the component: labels, aria-labels, screen reader announcements.
English by default. Change it with the messages prop of YugenProvider, by the namespace below; messagesVi has the Vietnamese set. See Interface language.
messages.appShell
| Key · Type | Default (English) | messagesVi |
|---|---|---|
| skipToContentstring Skip link đầu trang, tới AppShellMain. | Default (English)"Skip to content" | messagesVi"Chuyển tới nội dung" |
| sidebarstring Tên của Drawer chứa sidebar trên màn hẹp. | Default (English)"Sidebar" | messagesVi"Thanh bên" |
| openSidebarstring SidebarTrigger trên màn hẹp. | Default (English)"Open sidebar" | messagesVi"Mở thanh bên" |
| collapseSidebarstring SidebarTrigger và SidebarRail trên màn rộng. | Default (English)"Collapse sidebar" | messagesVi"Thu gọn thanh bên" |
| expandSidebarstring | Default (English)"Expand sidebar" | messagesVi"Mở rộng thanh bên" |
| resizeSidebarstring Thanh chia kéo đổi độ rộng (Sidebar | Default (English)"Resize sidebar" | messagesVi"Đổi độ rộng thanh bên" |