App Shell
A composable application shell with sidebar, header, main, and aside
import { AppShell } from '@/components/app-shell';
export const meta = {
layout: 'fullscreen',
} as const;
export default function AppShellPreview() {
return (
<div className="h-full w-full overflow-hidden">
<AppShell>
<AppShell.Sidebar>
<div className="p-4 text-foreground-secondary text-sm">Sidebar</div>
</AppShell.Sidebar>
<AppShell.Main>
<AppShell.Header>
<span className="font-medium">Dashboard</span>
</AppShell.Header>
<AppShell.Content>
<div className="p-6 text-foreground-secondary text-sm">
Main content
</div>
</AppShell.Content>
</AppShell.Main>
</AppShell>
</div>
);
} Dependencies
Source Code
'use client';
import { createContext, use, useState } from 'react';
import { Slot } from '@/components/slot';
import { cn } from '@/lib/utils/classnames';
interface AppShellContextValue {
sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void;
mobileOpen: boolean;
setMobileOpen: (open: boolean) => void;
}
const AppShellContext = createContext<AppShellContextValue | null>(null);
const useAppShell = () => {
const ctx = use(AppShellContext);
if (!ctx) throw new Error('AppShell components must be inside <AppShell>');
return ctx;
};
interface AppShellProps extends React.ComponentPropsWithRef<'div'> {
defaultSidebarCollapsed?: boolean;
asChild?: boolean;
}
const AppShell = ({
ref,
className,
defaultSidebarCollapsed = false,
asChild,
...rest
}: AppShellProps) => {
const [sidebarCollapsed, setSidebarCollapsed] = useState(
defaultSidebarCollapsed
);
const [mobileOpen, setMobileOpen] = useState(false);
const Comp = asChild ? Slot : 'div';
return (
<AppShellContext
value={{
sidebarCollapsed,
setSidebarCollapsed,
mobileOpen,
setMobileOpen,
}}
>
<Comp
ref={ref}
data-app-shell
data-sidebar-collapsed={sidebarCollapsed ? '' : undefined}
className={cn(
'relative flex h-svh w-full overflow-hidden text-foreground',
className
)}
{...rest}
/>
</AppShellContext>
);
};
interface AppShellSidebarProps extends React.ComponentPropsWithRef<'aside'> {
/** Width when expanded. Default 16rem. */
width?: string;
/** Width when collapsed. Default 3.5rem. */
collapsedWidth?: string;
}
const AppShellSidebar = ({
ref,
className,
width = '16rem',
collapsedWidth = '3.5rem',
style,
...rest
}: AppShellSidebarProps) => {
const { sidebarCollapsed, mobileOpen, setMobileOpen } = useAppShell();
return (
<>
{/* Mobile backdrop */}
{mobileOpen && (
<button
type="button"
aria-label="Close sidebar"
onClick={() => setMobileOpen(false)}
className="fixed inset-0 z-40 md:hidden"
/>
)}
<aside
ref={ref}
data-app-shell-sidebar
style={
{
'--sidebar-w': width,
'--sidebar-w-collapsed': collapsedWidth,
...style,
} as React.CSSProperties
}
className={cn(
'fixed inset-y-0 left-0 z-50 flex shrink-0 flex-col border-border border-r bg-background',
'transition-[width,transform] duration-(--duration-hover) ease-(--ease)',
'w-(--sidebar-w)',
mobileOpen ? 'translate-x-0' : '-translate-x-full',
'md:static md:translate-x-0',
// collapsed width — driven by the data attribute on the AppShell root
sidebarCollapsed && 'md:w-(--sidebar-w-collapsed)',
className
)}
{...rest}
/>
</>
);
};
const AppShellHeader = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'header'>) => (
<header
ref={ref}
data-app-shell-header
className={cn(
'flex h-14 shrink-0 items-center gap-3 border-border border-b bg-background px-4 md:h-16 md:px-6',
className
)}
{...rest}
/>
);
interface AppShellMainProps extends React.ComponentPropsWithRef<'main'> {
/**
* Renders the main region as a floating, rounded card inset from the page
* edges (shadcn-style "inset" sidebar layout). The page background shows
* through around all sides.
*/
inset?: boolean;
/** Override className for the inset card wrapper (only used when `inset`). */
insetClassName?: string;
}
const AppShellMain = ({
ref,
className,
inset,
insetClassName,
children,
...rest
}: AppShellMainProps) => (
<main
ref={ref}
data-app-shell-main
data-inset={inset ? '' : undefined}
className={cn(
'flex min-w-0 flex-1 flex-col overflow-hidden',
inset && 'p-2 md:p-3',
className
)}
{...rest}
>
{inset ? (
<div
className={cn(
'flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-border/50 bg-surface-default shadow-sm',
insetClassName
)}
>
{children}
</div>
) : (
children
)}
</main>
);
const AppShellContent = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
data-app-shell-content
className={cn('min-h-0 flex-1 overflow-auto ', className)}
{...rest}
/>
);
const AppShellAside = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'aside'>) => (
<aside
ref={ref}
data-app-shell-aside
className={cn(
'hidden w-80 shrink-0 overflow-auto border-border border-l lg:block',
className
)}
{...rest}
/>
);
const CompoundAppShell = Object.assign(AppShell, {
Sidebar: AppShellSidebar,
Header: AppShellHeader,
Main: AppShellMain,
Content: AppShellContent,
Aside: AppShellAside,
});
export type { AppShellMainProps, AppShellProps, AppShellSidebarProps };
export { CompoundAppShell as AppShell, useAppShell }; AppShell is the top-level layout for productivity apps and dashboards. It
composes a collapsible sidebar, a sticky header, the main content region, and
an optional right-hand aside. On small screens the sidebar slides in over a
backdrop.
Anatomy
<AppShell>
<AppShell.Sidebar>{nav}</AppShell.Sidebar>
<AppShell.Main>
<AppShell.Header>{topbar}</AppShell.Header>
<AppShell.Content>{page}</AppShell.Content>
</AppShell.Main>
<AppShell.Aside>{panel}</AppShell.Aside>
</AppShell>
API Reference
AppShell
Extends the div element.
| Prop | Default | Type | Description |
|---|---|---|---|
defaultSidebarCollapsed | false | boolean | |
asChild | false | boolean | Render the shell root as the provided child element instead of a `<div>` — e.g. a framework `<Layout>` wrapper. |
asChild
The shell root is a plain <div> so it can host any framework wrapper.
Pass asChild to merge the shell’s layout styles and context onto a
consumer-supplied element (for example, a Next.js layout wrapper or a
motion.div) without forking the component.
<AppShell asChild>
<div data-page="dashboard">
<AppShell.Sidebar>{nav}</AppShell.Sidebar>
<AppShell.Main>
<AppShell.Header>{topbar}</AppShell.Header>
<AppShell.Content>{page}</AppShell.Content>
</AppShell.Main>
</div>
</AppShell>
AppShell.Sidebar
Extends the aside element.
| Prop | Default | Type |
|---|---|---|
width | "16rem" | string |
collapsedWidth | "3.5rem" | string |
AppShell.Header
Extends the header element.
AppShell.Main
Extends the main element.
| Prop | Default | Type |
|---|---|---|
inset | false | boolean |
When inset is set, the main region renders as a floating, rounded card with
an outer margin — the shadcn-style “inset sidebar” layout. Pair with a
transparent, borderless <AppShell.Sidebar> so the page tint shows through
behind it.
AppShell.Content
Extends the div element. Scrollable region inside Main.
AppShell.Aside
Extends the aside element. Hidden below lg.
Examples
Default
import { AppShell } from '@/components/app-shell';
export const meta = {
layout: 'fullscreen',
} as const;
export default function AppShellPreview() {
return (
<div className="h-full w-full overflow-hidden">
<AppShell>
<AppShell.Sidebar>
<div className="p-4 text-foreground-secondary text-sm">Sidebar</div>
</AppShell.Sidebar>
<AppShell.Main>
<AppShell.Header>
<span className="font-medium">Dashboard</span>
</AppShell.Header>
<AppShell.Content>
<div className="p-6 text-foreground-secondary text-sm">
Main content
</div>
</AppShell.Content>
</AppShell.Main>
</AppShell>
</div>
);
} Inset (floating dashboard)
Dashboard
Track activity and performance across your workspace.
import {
Home01,
Inbox01,
LineChartUp01,
Plus,
Settings01,
TrendUp01,
Users01,
} from '@untitledui-pro/icons/solid';
import { AppShell } from '@/components/app-shell';
import { Badge } from '@/components/badge';
import { Button } from '@/components/button';
import { Container } from '@/components/container';
import { Grid } from '@/components/grid';
import { Nav } from '@/components/nav';
import { PageHeader } from '@/components/page-header';
import { Stat } from '@/components/stat';
import { Surface } from '@/components/surface';
export const meta = {
layout: 'fullscreen',
} as const;
export default function AppShellInsetPreview() {
return (
<div className="h-full w-full overflow-hidden">
<AppShell>
<AppShell.Sidebar width="13rem" className="border-r-0 bg-transparent">
<div className="flex h-14 shrink-0 items-center gap-2 px-4 font-semibold md:h-16">
Acme
</div>
<Nav>
<Nav.Group label="Workspace">
<Nav.Item href="#" active>
<Nav.Icon>
<Home01 />
</Nav.Icon>
<Nav.Label>Overview</Nav.Label>
</Nav.Item>
<Nav.Item href="#">
<Nav.Icon>
<Inbox01 />
</Nav.Icon>
<Nav.Label>Inbox</Nav.Label>
<Nav.Trailing>
<Badge size="xs" variant="neutral">
12
</Badge>
</Nav.Trailing>
</Nav.Item>
<Nav.Item href="#">
<Nav.Icon>
<LineChartUp01 />
</Nav.Icon>
<Nav.Label>Analytics</Nav.Label>
</Nav.Item>
<Nav.Item href="#">
<Nav.Icon>
<Users01 />
</Nav.Icon>
<Nav.Label>Team</Nav.Label>
</Nav.Item>
</Nav.Group>
<Nav.Group label="Settings">
<Nav.Item href="#">
<Nav.Icon>
<Settings01 />
</Nav.Icon>
<Nav.Label>Preferences</Nav.Label>
</Nav.Item>
</Nav.Group>
</Nav>
</AppShell.Sidebar>
<AppShell.Main inset>
<AppShell.Header>
<span className="font-medium">Overview</span>
<div className="ml-auto flex items-center gap-2">
<Button variant="outline" size="sm">
Filter
</Button>
<Button size="sm">
<Plus />
New
</Button>
</div>
</AppShell.Header>
<AppShell.Content className="bg-transparent">
<Container className="mx-auto max-w-4xl py-8" gutter="md">
<PageHeader>
<PageHeader.Content>
<PageHeader.Title>Dashboard</PageHeader.Title>
<PageHeader.Description>
Track activity and performance across your workspace.
</PageHeader.Description>
</PageHeader.Content>
</PageHeader>
{/* KPI tiles — elevation="raised" sit on the inset card */}
<Grid className="mt-8">
<Grid.Col span={4}>
<Surface elevation="raised">
<Stat>
<Stat.Label>Active users</Stat.Label>
<Stat.Value>12,480</Stat.Value>
<Stat.Trend direction="up">
<TrendUp01 /> +8.2%
</Stat.Trend>
</Stat>
</Surface>
</Grid.Col>
<Grid.Col span={4}>
<Surface elevation="raised">
<Stat>
<Stat.Label>Revenue</Stat.Label>
<Stat.Value>$48.2k</Stat.Value>
<Stat.Description>vs $44.1k last month</Stat.Description>
</Stat>
</Surface>
</Grid.Col>
<Grid.Col span={4}>
<Surface elevation="raised">
<Stat>
<Stat.Label>Churn</Stat.Label>
<Stat.Value>1.4%</Stat.Value>
<Stat.Trend direction="down">−0.3%</Stat.Trend>
</Stat>
</Surface>
</Grid.Col>
{/* Recent activity panel — raised, with overlay-elevation rows nested inside */}
<Grid.Col span="full">
<Surface elevation="raised" padding="lg">
<div className="mb-4 flex items-center justify-between">
<div className="font-medium">Recent activity</div>
<Button size="xs" variant="ghost">
View all
</Button>
</div>
<div className="flex flex-col gap-2">
<Surface elevation="overlay" padding="sm">
<div className="flex items-center justify-between">
<span className="text-sm">
<span className="font-medium">Maya</span> shipped
v1.4.0
</span>
<span className="text-foreground-secondary text-xs">
2m ago
</span>
</div>
</Surface>
<Surface elevation="overlay" padding="sm">
<div className="flex items-center justify-between">
<span className="text-sm">
<span className="font-medium">Rahul</span> opened 12
issues
</span>
<span className="text-foreground-secondary text-xs">
18m ago
</span>
</div>
</Surface>
<Surface elevation="overlay" padding="sm">
<div className="flex items-center justify-between">
<span className="text-sm">
<span className="font-medium">Sofia</span> merged 3
PRs
</span>
<span className="text-foreground-secondary text-xs">
1h ago
</span>
</div>
</Surface>
</div>
</Surface>
</Grid.Col>
</Grid>
</Container>
</AppShell.Content>
</AppShell.Main>
</AppShell>
</div>
);
} Previous
Setup
Next
Auto Grid