Shell
A composable macOS app shell for source lists, toolbars, split panes, and main content.
Shell is the window-level layout primitive for Acrylic apps. It owns the
geometry of the app frame: source-list columns, split panes, toolbars, and
scrollable content. Sidebar primitives can be used inside that frame for
source-list content and styling, but Shell remains the layout owner.
This boundary matters: Sidebar's collapsible layout reads --sidebar-width
and --sidebar-width-icon to size both its layout reservation and visible rail.
Inside Shell, those geometry tokens should come from Shell, not ad-hoc width
classes on the Sidebar itself. That keeps the sidebar gap, visible surface, and
collapsed icon rail aligned.
Installation
npx shadcn add https://acrylic-ui.vercel.app/r/shell.jsonUsage
import {
Shell,
ShellBody,
ShellContent,
ShellInset,
ShellNavbar,
ShellNavbarActions,
ShellNavbarTitle,
ShellPanel,
ShellPanelDescription,
ShellPanelHeader,
ShellPanelTitle,
} from "@/components/acrylic/shell"
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/acrylic/sidebar"
<Shell sidebarWidth={150} sidebarCollapsedWidth={48}>
<Sidebar collapsible="icon">
<SidebarHeader>{/* source list header */}</SidebarHeader>
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton isActive>Inbox</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
<ShellInset>
<ShellNavbar>
<ShellNavbarTitle>All Inboxes</ShellNavbarTitle>
<ShellNavbarActions>{/* global actions */}</ShellNavbarActions>
</ShellNavbar>
<ShellBody>
<ShellPanel variant="list">
<ShellPanelHeader>
<div className="min-w-0">
<ShellPanelTitle>Inbox</ShellPanelTitle>
<ShellPanelDescription>67 messages</ShellPanelDescription>
</div>
</ShellPanelHeader>
<ShellContent padding="flush">{/* list */}</ShellContent>
</ShellPanel>
<ShellPanel variant="detail">
<ShellPanelHeader>
<div className="min-w-0">
<ShellPanelTitle>Message title</ShellPanelTitle>
<ShellPanelDescription>sender@example.com</ShellPanelDescription>
</div>
</ShellPanelHeader>
<ShellContent padding="reading">{/* detail */}</ShellContent>
</ShellPanel>
</ShellBody>
</ShellInset>
</Shell>Composition
Shell
├── Sidebar (source-list region; width tokens owned by Shell)
│ ├── SidebarHeader
│ └── SidebarContent
└── ShellInset
├── ShellNavbar
└── ShellBody
├── ShellPanel
│ ├── ShellPanelHeader
│ └── ShellContent
└── ShellPanel
├── ShellPanelHeader
└── ShellContentExamples
Compose a Mail-style split view: source list, message list, and reading pane.
"use client"import * as React from "react"import { Archive, ArchiveX, ChevronLeft, ChevronRight, Clock, Flag, Forward, Inbox, Maximize2, Minimize2, Paperclip, Reply, Send, SquarePen, Star, Trash2, type LucideIcon,} from "lucide-react"import { ExampleBackdrop } from "@/components/example-backdrop"import { Button } from "@/registry/acrylic/button"import { Item, ItemAction, ItemContent, ItemDescription, ItemMeta, ItemRow, ItemSeparator, ItemTitle,} from "@/registry/acrylic/item"import { Shell, ShellBody, ShellContent, ShellInset, ShellNavbar, ShellPanel, ShellPanelHeader,} from "@/registry/acrylic/shell"import { Sidebar, SidebarContent, SidebarHeader, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarTrigger,} from "@/registry/acrylic/sidebar"const MAILBOXES = [ { title: "Inbox", count: 18, icon: Inbox, active: true }, { title: "VIP", count: 4, icon: Star }, { title: "Sent", count: 0, icon: Send }, { title: "Later", count: 7, icon: Clock }, { title: "Archive", count: 0, icon: Archive }, { title: "Trash", count: 0, icon: Trash2 },]const MESSAGES = [ { from: "Acrylic UI", subject: "Registry review notes", preview: "The Shell primitive keeps app structure separate from Sidebar.", time: "9:42 AM", marker: Flag, active: true, initials: "AU", image: "https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=900&q=80", body: [ "Shell owns the window structure. Sidebar remains the source-list primitive, while the main panel can split into list and detail panes at exact macOS frame sizes.", "This Mail example uses Item for each message preview, so density, truncation, and actions can be reused without turning Shell into a mail-specific API.", ], }, { from: "Mira Chen", subject: "Window shell sketches", preview: "I added a Mail-style split view and a Notes editor variant.", time: "8:15 AM", marker: Paperclip, initials: "MC", image: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=900&q=80", body: [ "The split view is now close enough to the Mail reference that the remaining work is mostly behavioral polish.", "The important part is keeping the message list as Item composition, not a mail-only component.", ], }, { from: "Design Systems", subject: "Source list density", preview: "Keep sidebar rows compact at 13px with muted counts.", time: "Yesterday", compact: true, initials: "DS", image: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=900&q=80", body: [ "Density should come from stable primitives: fixed row heights, truncation rules, and predictable action slots.", "The same item anatomy can carry mail previews, command history, or document lists without changing Shell.", ], }, { from: "Tauri", subject: "Vibrancy pass", preview: "The backdrop is visible through the shell and sidebar surfaces.", time: "Mon", marker: Star, initials: "TA", image: "https://images.unsplash.com/photo-1519608487953-e999c86e7455?auto=format&fit=crop&w=900&q=80", body: [ "The web demo and native shell share the same composition, but the material source changes underneath.", "That keeps the component API honest: Shell describes structure, while the host supplies the window environment.", ], }, { from: "Release", subject: "Docs capture", preview: "The preview should match the macOS mail frame before API polish.", time: "Jun 22", marker: Flag, initials: "RE", image: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&w=900&q=80", body: [ "The docs capture should show the product surface first: sidebar, list, detail, toolbar, and live theme switching.", "Once the frame reads correctly, the API story becomes much easier to explain.", ], }, { from: "Preview Bot", subject: "Screenshot run", preview: "Playwright has a repeatable path for the docs shell page.", time: "Jun 21", initials: "PB", image: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=900&q=80", body: [ "The verification script checks that the shell, sidebar, navbar, and inbox panes render without horizontal page overflow.", "It also captures both the default board and Inbox view so visual regressions are easier to compare.", ], },]function AppleIcon({ icon: Icon, size = 14, strokeWidth = 1.5,}: { icon: LucideIcon size?: number strokeWidth?: number}) { return ( <Icon aria-hidden absoluteStrokeWidth size={size} strokeWidth={strokeWidth} /> )}function MailListItem({ message, selected, onSelect,}: { message: (typeof MESSAGES)[number] selected: boolean onSelect: () => void}) { return ( <Item asChild size="xs" selected={selected} className={[ "w-full rounded-none px-3 py-2", "items-start gap-0 text-left", "focus-visible:relative focus-visible:z-10", ] .filter(Boolean) .join(" ")} > <button type="button" className="w-full" onClick={onSelect}> <ItemContent className="flex min-w-0 flex-col"> <ItemRow className="h-4 items-center"> <ItemTitle className="min-w-0 flex-1 text-[13px] font-semibold leading-4"> {message.from} </ItemTitle> <ItemMeta className="text-[11px] leading-4"> {message.time} </ItemMeta> </ItemRow> <ItemRow className="mt-1 h-[13px] items-center"> <ItemTitle className="min-w-0 flex-1 text-[11px] font-medium leading-[13px]"> {message.subject} </ItemTitle> <ItemAction className="h-3 w-3 text-muted-foreground [&>svg]:size-3"> {message.marker ? ( <AppleIcon icon={message.marker} size={12} strokeWidth={1.4} /> ) : null} </ItemAction> </ItemRow> <ItemDescription className={[ "mt-1 text-[11px] leading-[13px]", message.compact ? "line-clamp-1" : "line-clamp-2", ].join(" ")} > {message.preview} </ItemDescription> </ItemContent> </button> </Item> )}export default function ShellMail() { const [expanded, setExpanded] = React.useState(false) const [selectedSubject, setSelectedSubject] = React.useState(MESSAGES[0].subject) const ExpandIcon = expanded ? Minimize2 : Maximize2 const selectedMessage = MESSAGES.find((message) => message.subject === selectedSubject) ?? MESSAGES[0] return ( <ExampleBackdrop className={expanded ? "min-h-0" : undefined}> <div className={ expanded ? "fixed inset-0 z-50 flex bg-background" : "relative w-full max-w-5xl" } > <Shell variant="inset" sidebarWidth={150} sidebarCollapsedWidth={48} className={[ "transition-all duration-200 ease-out", expanded ? "h-full w-full rounded-none shadow-none" : "h-[500px] w-[800px] shrink-0 shadow-[0_18px_54px_rgba(0,0,0,0.28)]", ].join(" ")} > <Button icon size="large" variant="neutral" aria-label={expanded ? "Shrink demo" : "Expand demo"} aria-pressed={expanded} title={expanded ? "Shrink demo" : "Expand demo"} onClick={() => setExpanded((value) => !value)} className="absolute bottom-3 left-3 z-20 text-foreground" > <ExpandIcon /> </Button> <Sidebar collapsible="icon" className={expanded ? undefined : "rounded-l-xl"} > <SidebarHeader className="h-[46px] px-2 pt-4"> <div className="flex h-7 items-center gap-2"> <div className="min-w-0 flex-1 truncate text-[12px] font-semibold group-data-[collapsible=icon]:hidden"> Mailboxes </div> <SidebarTrigger aria-label="Collapse mailboxes" /> </div> </SidebarHeader> <SidebarContent className="scrollbar-mac px-2 pb-2 pt-0"> <SidebarMenu> {MAILBOXES.map((mailbox) => ( <SidebarMenuItem key={mailbox.title}> <SidebarMenuButton isActive={mailbox.active} size="sm"> <mailbox.icon /> <span>{mailbox.title}</span> </SidebarMenuButton> {mailbox.count > 0 ? ( <SidebarMenuBadge>{mailbox.count}</SidebarMenuBadge> ) : null} </SidebarMenuItem> ))} </SidebarMenu> </SidebarContent> </Sidebar> <ShellInset> <ShellBody> <ShellPanel variant="list" className="w-[225px] flex-none"> <ShellPanelHeader className="h-[49px] px-4"> <div className="min-w-0 flex-1"> <div className="truncate text-[15px] font-semibold leading-5"> Inbox </div> <div className="truncate text-[11px] leading-[14px] text-muted-foreground"> 67 messages </div> </div> <Button variant="ghost" size="large" aria-label="New message" className="h-7 w-9 rounded-[6px] px-0" > <SquarePen /> </Button> </ShellPanelHeader> <ShellContent padding="flush" className="scrollbar-mac"> {MESSAGES.map((message, index) => { const nextMessage = MESSAGES[index + 1] const showSeparator = nextMessage && message.subject !== selectedSubject && nextMessage.subject !== selectedSubject return ( <React.Fragment key={message.subject}> <MailListItem message={message} selected={message.subject === selectedSubject} onSelect={() => setSelectedSubject(message.subject)} /> {showSeparator ? <ItemSeparator /> : null} </React.Fragment> ) })} </ShellContent> </ShellPanel> <ShellPanel variant="detail" className="min-w-[425px] flex-1" > <ShellNavbar className="h-[49px] justify-between px-1.5"> <div className="flex h-7 items-center gap-0.5"> <Button icon variant="ghost" size="large" aria-label="Archive"> <Archive strokeWidth={1.5} /> </Button> <Button icon variant="ghost" size="large" aria-label="Archive junk" > <ArchiveX strokeWidth={1.5} /> </Button> <Button icon variant="ghost" size="large" aria-label="Delete"> <Trash2 strokeWidth={1.5} /> </Button> </div> <div className="flex h-7 items-center gap-6"> <div className="flex h-7 items-center gap-0.5"> <Button icon variant="ghost" size="large" aria-label="Reply"> <Reply strokeWidth={1.5} /> </Button> <Button icon variant="ghost" size="large" aria-label="Forward" > <Forward strokeWidth={1.5} /> </Button> <Button icon variant="ghost" size="large" aria-label="Flag"> <Flag strokeWidth={1.5} /> </Button> </div> <div className="flex items-center gap-1"> <Button variant="ghost" size="medium" aria-label="Previous message" className="h-7 w-9 rounded-[6px] px-0" > <ChevronLeft strokeWidth={1.5} /> </Button> <Button variant="ghost" size="medium" aria-label="Next message" className="h-7 w-9 rounded-[6px] px-0" > <ChevronRight strokeWidth={1.5} /> </Button> </div> </div> </ShellNavbar> <ShellContent padding="flush" className="scrollbar-mac px-5 pt-4"> <article className="w-full"> <header className="h-[67px] border-b border-[var(--acr-border-soft)]"> <div className="flex h-[50px] gap-3"> <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-[var(--acr-chip)] text-[11px] font-semibold text-foreground"> {selectedMessage.initials} </span> <div className="min-w-0 flex-1"> <div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-5"> <div className="truncate text-[13px] font-semibold leading-4"> {selectedMessage.from} </div> <div className="pt-px text-[11px] leading-[13px] text-muted-foreground whitespace-nowrap shrink-0"> {selectedMessage.time} </div> </div> <div className="mt-1 grid grid-cols-[minmax(0,1fr)_12px] items-start gap-2"> <div className="truncate text-[11px] font-medium leading-[13px]"> {selectedMessage.subject} </div> <span className="mt-px text-muted-foreground"> {selectedMessage.marker ? ( <AppleIcon icon={selectedMessage.marker} size={12} strokeWidth={1.4} /> ) : null} </span> </div> <div className="mt-1 truncate text-[11px] leading-[13px] text-muted-foreground"> {selectedMessage.preview} </div> </div> </div> </header> <div className="flex flex-col gap-4 pt-4"> <div className="text-[13px] leading-6 text-foreground/85"> {selectedMessage.body.map((paragraph) => ( <p key={paragraph} className="mt-3 first:mt-0"> {paragraph} </p> ))} </div> <div className="aspect-[16/10] w-full overflow-hidden rounded-[8px] border border-[var(--acr-border-soft)] bg-[var(--acr-card-nested)] shadow-[inset_0_1px_rgba(255,255,255,0.14)]"> <img src={selectedMessage.image} alt="" className="h-full w-full object-cover" /> </div> </div> </article> </ShellContent> </ShellPanel> </ShellBody> </ShellInset> </Shell> </div> </ExampleBackdrop> )}Notes
Use the same structure for a Notes-style source list, note list, and editor.
"use client"import * as React from "react"import { Folder, Maximize2, Minimize2, PenLine, Search } from "lucide-react"import { ExampleBackdrop } from "@/components/example-backdrop"import { Button } from "@/registry/acrylic/button"import { Item, ItemContent, ItemDescription, ItemSeparator, ItemTitle,} from "@/registry/acrylic/item"import { Shell, ShellBody, ShellContent, ShellInset, ShellNavbar, ShellNavbarActions, ShellNavbarHeading, ShellNavbarSubtitle, ShellNavbarTitle, ShellPanel,} from "@/registry/acrylic/shell"import { Sidebar, SidebarContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem,} from "@/registry/acrylic/sidebar"const FOLDERS = ["All iCloud", "Acrylic", "Components", "Archive"]const NOTES = [ { title: "Shell component boundary", preview: "Shell is a composable layout primitive. App names belong in examples, not exports.", body: [ "A shell describes the app frame: sidebar, inset, panels, navbar, and scrollable content. Users compose those pieces into Mail, Notes, or their own product surface.", "Panel headers are for local list metadata. Reading and editing surfaces should let the document title live inside the content.", ], }, { title: "macOS Mail split view", preview: "The Mail example reuses the same Item composition for its message list.", body: [ "Density, truncation, and selection come from Item, not from a mail-only row component. Notes should read the same list.", ], }, { title: "Notes editor padding", preview: "The reading pane uses ShellContent padding=\"reading\" for prose margins.", body: [ "The editor column caps its width and relies on ShellContent's reading padding rather than a bespoke max-width wrapper.", ], }, { title: "Registry docs outline", preview: "Shell's API Reference should mirror Card's per-part prop tables.", body: [ "Every Shell sub-part renders a plain element with a data-slot hook and forwards native props — the docs should say so explicitly per part.", ], },]export default function ShellNotes() { const [expanded, setExpanded] = React.useState(false) const [selectedTitle, setSelectedTitle] = React.useState(NOTES[0].title) const ExpandIcon = expanded ? Minimize2 : Maximize2 const selectedNote = NOTES.find((note) => note.title === selectedTitle) ?? NOTES[0] return ( <ExampleBackdrop className={expanded ? "min-h-0" : undefined}> <div className={ expanded ? "fixed inset-0 z-50 flex bg-background" : "relative w-full max-w-5xl" } > <Shell variant="inset" className={ expanded ? "h-full w-full rounded-none shadow-none dark:shadow-none [&_[data-slot=sidebar]]:rounded-none" : "h-[31rem] w-full" } > <Button icon size="large" variant="neutral" aria-label={expanded ? "Shrink demo" : "Expand demo"} aria-pressed={expanded} title={expanded ? "Shrink demo" : "Expand demo"} onClick={() => setExpanded((value) => !value)} className="absolute bottom-3 left-3 z-20 text-foreground" > <ExpandIcon /> </Button> <Sidebar collapsible="none" className="w-52 rounded-l-xl"> <SidebarHeader> <div className="flex h-11 items-center px-2"> <ShellNavbarTitle>Folders</ShellNavbarTitle> </div> </SidebarHeader> <SidebarContent className="scrollbar-mac p-2"> <SidebarMenu> {FOLDERS.map((folder, index) => ( <SidebarMenuItem key={folder}> <SidebarMenuButton isActive={index === 1}> <Folder /> <span>{folder}</span> </SidebarMenuButton> </SidebarMenuItem> ))} </SidebarMenu> </SidebarContent> </Sidebar> <ShellInset> <ShellNavbar size="large" className="px-4"> <ShellNavbarHeading> <ShellNavbarTitle className="text-[15px] leading-5">Notes</ShellNavbarTitle> <ShellNavbarSubtitle className="leading-[14px]">4 notes</ShellNavbarSubtitle> </ShellNavbarHeading> <ShellNavbarActions> <div className="flex h-7 w-40 items-center gap-2 rounded-[7px] bg-[var(--acr-field)] px-2 text-[12px] text-muted-foreground"> <Search className="size-3.5" /> <span>Search</span> </div> <Button variant="ghost" size="small" icon aria-label="New note"> <PenLine /> </Button> </ShellNavbarActions> </ShellNavbar> <ShellBody> <ShellPanel variant="list" className="w-56"> <ShellContent padding="flush" className="scrollbar-mac"> {NOTES.map((note, index) => { const nextNote = NOTES[index + 1] const showSeparator = nextNote && note.title !== selectedTitle && nextNote.title !== selectedTitle return ( <React.Fragment key={note.title}> <Item asChild size="xs" selected={note.title === selectedTitle} className={[ "w-full rounded-none px-3 py-2", "items-start gap-0 text-left", "focus-visible:relative focus-visible:z-10", ].join(" ")} > <button type="button" className="w-full" onClick={() => setSelectedTitle(note.title)} > <ItemContent className="flex min-w-0 flex-col"> <ItemTitle className="min-w-0 text-[13px] font-semibold leading-4"> {note.title} </ItemTitle> <ItemDescription className="mt-1 text-[11px] leading-[13px]"> {note.preview} </ItemDescription> </ItemContent> </button> </Item> {showSeparator ? <ItemSeparator /> : null} </React.Fragment> ) })} </ShellContent> </ShellPanel> <ShellPanel variant="detail"> <ShellContent padding="reading" className="scrollbar-mac"> <article className="mx-auto max-w-2xl space-y-4"> <div className="space-y-1"> <h3 className="text-[24px] font-semibold leading-tight"> {selectedNote.title} </h3> <p className="text-[12px] text-muted-foreground"> Edited today </p> </div> {selectedNote.body.map((paragraph) => ( <p key={paragraph} className="text-[13px] leading-6 text-foreground/85" > {paragraph} </p> ))} </article> </ShellContent> </ShellPanel> </ShellBody> </ShellInset> </Shell> </div> </ExampleBackdrop> )}Anatomy
| part | role |
|---|---|
Shell | the outer app frame and geometry owner; pass variant="inset" for a rounded preview/window surface |
Sidebar | source-list region inside the shell; may use collapsible, but Shell owns sidebarWidth and sidebarCollapsedWidth |
ShellInset | the main region beside the sidebar |
ShellNavbar | an inset-level navigation/toolbar row that can span multiple panels |
ShellBody | the horizontal region below an inset-level navbar |
ShellPanel | a split pane inside the inset; adjacent panels share one divider |
ShellPanelHeader | a per-panel title/meta strip below the navbar |
ShellNavbarTitle / ShellNavbarSubtitle | compact title text for pane chrome |
ShellPanelTitle / ShellPanelDescription | compact title/meta text for panel headers |
ShellNavbarActions | right-aligned action cluster with standard spacing |
ShellContent | the scrollable body; defaults to padding="flush"; pass default or reading for built-in spacing |
Notes
Shellowns pane geometry. Source-list width, collapsed width, and adjacent panel flow should be decided at the Shell layer. PasssidebarWidthandsidebarCollapsedWidthto Shell so Sidebar's gap reservation and visible rail read the same geometry source.- Avoid setting source-list width only with
classNameonSidebarinside Shell: that can resize the visible rail without updating Sidebar's internal gap reservation. - Sidebar content primitives (
SidebarHeader,SidebarContent,SidebarMenuButton, badges, groups) are still the right vocabulary for source-list rows inside Shell. - The panel variants set common structural widths only. Override widths with
classNamewhen the app needs a different split. - Put
ShellNavbardirectly underShellInsetwhen the toolbar should span multiple panels, as in Mail. UseShellPanelHeaderfor per-panel title and metadata rows. ShellNavbarandShellPanelHeaderdraw their own horizontal bottom rules.ShellPanelowns vertical dividers, so two adjacent panels do not double their borders.- List panels (
ShellPanel variant="list"+ShellContent padding="flush") are not a separate list API — rows areItemcompositions (ItemContent/ItemTitle/ItemDescription/ItemMeta), selection isItem's ownselectedprop, and rows are divided withItemSeparator. Mail and Notes both compose the sameItemanatomy; see their source for the row + selection + separator pattern.
API Reference
Every Shell sub-part renders a plain element with a data-slot styling hook and
forwards all native props (className, onClick, style, …). className is for
layout only — color always flows through the --acr-* tokens. Three legacy aliases
ship for back-compat: ShellPane (= ShellPanel), ShellToolbar (= ShellNavbar),
ShellTitle (= ShellNavbarTitle) — prefer the primary names in new code.
Shell
The outer app frame and geometry owner. Wraps SidebarProvider, so defaultOpen /
open / onOpenChange control the sidebar's collapsed state. Renders
<div data-slot="shell" data-variant>.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "inset" | "default" | "inset" adds the rounded frosted-glass window surface (acr-frosted, blur, shadow) used for preview/window contexts; "default" is a flat full-bleed frame. |
sidebarWidth | number | string | — | Sets --sidebar-width (numbers are treated as px). Read by Sidebar's own layout — set it here, not as a className width on Sidebar. |
sidebarCollapsedWidth | number | string | — | Sets --sidebar-width-icon, the collapsed icon-rail width. |
defaultOpen / open / onOpenChange | SidebarProvider props | — | Forwarded to the underlying SidebarProvider to control the sidebar's open/collapsed state. |
className | string | — | Merged onto the frame (layout only). |
...props | React.ComponentProps<"div"> | — | Forwarded to the root <div>. |
ShellInset
The main region beside the sidebar — a flex column that clips overflow. Renders
<main data-slot="shell-inset">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the region. |
...props | React.ComponentProps<"main"> | — | Forwarded to the <main>. |
ShellNavbar
An inset-level toolbar row; place directly under ShellInset when it should span
every panel below it (as in Mail), or inside a single panel for per-panel chrome.
Draws its own bottom rule. Renders <div data-slot="shell-navbar" data-size>.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "compact" | "large" | "default" | Row height: compact h-10, default h-11, large h-[52px]. |
className | string | — | Merged onto the row. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellNavbarHeading
Flexible title/subtitle stack for ShellNavbar — takes the remaining row width.
Renders <div data-slot="shell-navbar-heading">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the stack. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellNavbarTitle / ShellNavbarSubtitle
Compact title (13px semibold) and subtitle (11px muted) text for navbar chrome,
each truncating. Render <div data-slot="shell-navbar-title"> /
<div data-slot="shell-navbar-subtitle">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the text. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellNavbarActions
Right-aligned action cluster (ml-auto) with standard spacing, for navbar
toolbar buttons. Renders <div data-slot="shell-navbar-actions">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the cluster. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellBody
The horizontal region below an inset-level navbar; lays out adjacent
ShellPanels in a row. Renders <div data-slot="shell-body">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the row. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellPanel
A split pane inside ShellBody. Adjacent panels share one vertical divider
([&+&]:border-l) so borders never double up. Renders
<section data-slot="shell-panel" data-variant>.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "list" | "detail" | "default" | "list" sets a fixed w-72 non-growing column (source/message/note lists); "detail" grows at flex-[1.35] (reading/detail panes). Override the width with className when the app needs a different split. |
className | string | — | Merged onto the pane. |
...props | React.ComponentProps<"section"> | — | Forwarded to the <section>. |
ShellPanelHeader
A per-panel title/meta strip below the navbar; draws its own bottom rule.
Renders <div data-slot="shell-panel-header">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the strip. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellPanelTitle / ShellPanelDescription
Compact title (13px semibold) and metadata (11px muted) text for panel
headers, each truncating. Render <div data-slot="shell-panel-title"> /
<div data-slot="shell-panel-description">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the text. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellPanelActions
Right-aligned action cluster (ml-auto) for ShellPanelHeader, the per-panel
counterpart to ShellNavbarActions. Renders <div data-slot="shell-panel-actions">.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the cluster. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |
ShellContent
The scrollable body of a panel. Renders <div data-slot="shell-content" data-padding>.
| Prop | Type | Default | Description |
|---|---|---|---|
padding | "flush" | "default" | "reading" | "flush" | "flush" adds no padding (for full-bleed lists composed of Item rows); "default" adds p-3; "reading" adds px-8 py-7 for prose/detail panes. |
className | string | — | Merged onto the scroll container. |
...props | React.ComponentProps<"div"> | — | Forwarded to the <div>. |