Drawer (side sheet)
A panel from the right that shows one record while the list stays in view. Used for application peeks, the review sheet and meetings.
Playground
Change the controls and press Open sheet. The code under the preview lists only the props you changed.
import { SheetContent } from '@/ui/sheet';<SheetContent />Over a list
Maya Chen's application. The sheet sits full height on the right with the overlay shadow on its open edge and no blanket, so the list behind stays usable. The header holds the record, then previous and next (K and J), then close. Stepping swaps the content in place.
import { useState } from 'react';
import { Avatar } from '@/ui/avatar';
import { Button } from '@/ui/button';
import { Lozenge } from '@/ui/lozenge';
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetHeader, SheetHeading, SheetTitle, SheetTrigger } from '@/ui/sheet';
const applications = [
{ name: 'Daniel Okafor', stage: 'CV sent', phase: 'client', owner: 'Joseph A.', next: 'None booked' },
{ name: 'Maya Chen', stage: 'Interview', phase: 'working', owner: 'Joseph A.', next: 'Mon 19 Oct' },
{ name: 'Priya Shah', stage: 'Offer', phase: 'offer', owner: 'Sara Okafor', next: 'Thu 22 Oct' },
] as const;
// The sheet shows one application while the list stays usable. J and K, or the arrows in its header, step through the list.
export default function SheetDefault() {
const [open, setOpen] = useState(false);
const [index, setIndex] = useState(1);
const app = applications[index];
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger render={<Button />}>Open Maya Chen</SheetTrigger>
<SheetContent>
<SheetHeader
onPrevious={index > 0 ? () => setIndex(index - 1) : undefined}
onNext={index < applications.length - 1 ? () => setIndex(index + 1) : undefined}
>
<Avatar name={app.name} size="large" />
<SheetHeading>
<SheetTitle>{app.name}</SheetTitle>
<SheetDescription>Senior Data Engineer · Halcyon Bank</SheetDescription>
</SheetHeading>
</SheetHeader>
<SheetBody>
<dl className="grid grid-cols-[140px_1fr] items-center gap-y-0 [&>*]:flex [&>*]:h-8 [&>*]:items-center">
<dt className="text-text-subtle">Stage</dt>
<dd>
<Lozenge phase={app.phase}>{app.stage}</Lozenge>
</dd>
<dt className="text-text-subtle">Owner</dt>
<dd className="font-medium">{app.owner}</dd>
<dt className="text-text-subtle">Next interview</dt>
<dd className="font-medium">{app.next}</dd>
</dl>
</SheetBody>
</SheetContent>
</Sheet>
);
}Phone navigation
The only left sheet, and the only one with a blanket: a click on the page closes it, and it traps focus while open.
import { ListIcon } from '@phosphor-icons/react';
import { IconButton } from '@/ui/button';
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetHeading, SheetTitle, SheetTrigger } from '@/ui/sheet';
const places = ['Home', 'Inbox', 'Jobs', 'Candidates', 'Companies', 'Meetings'];
// The phone navigation: the only left sheet, and the only one with a blanket. A click on the page closes it.
export default function SheetNavigation() {
return (
<Sheet modal>
<SheetTrigger
render={
<IconButton label="Open navigation" variant="secondary">
<ListIcon />
</IconButton>
}
/>
<SheetContent side="left" size="narrow">
<SheetHeader>
<SheetHeading>
<SheetTitle>Halcyon Recruitment</SheetTitle>
</SheetHeading>
</SheetHeader>
<SheetBody>
<nav aria-label="Main" className="flex flex-col gap-025">
{places.map((place) => (
<a key={place} href="#" className="flex h-7 items-center rounded-medium px-100 font-medium hover:bg-background-hovered">
{place}
</a>
))}
</nav>
</SheetBody>
</SheetContent>
</Sheet>
);
}When things go wrong
The header shows at once from what the row knows, and a long name cuts off with the full name in a tooltip. The details load under it; when they fail, the sheet says what happened and offers Retry, and the list behind still works. This one fails the first time.
import { useState } from 'react';
import { Avatar } from '@/ui/avatar';
import { Button } from '@/ui/button';
import { SectionMessage } from '@/ui/section-message';
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetHeader, SheetHeading, SheetTitle, SheetTrigger } from '@/ui/sheet';
import { Skeleton, SkeletonGroup } from '@/ui/skeleton';
const name = 'Nguyễn Thị Minh Khai-Montgomery O’Brien-Smith';
// The header shows at once from what the row knows; the details load. This one fails the first time, then Retry works.
export default function SheetUnhappy() {
const [state, setState] = useState<'loading' | 'error' | 'ready'>('loading');
const [tries, setTries] = useState(0);
const load = () => {
setState('loading');
setTimeout(() => setState(tries === 0 ? 'error' : 'ready'), 1200);
setTries(tries + 1);
};
return (
<Sheet onOpenChange={(open) => open && load()}>
<SheetTrigger render={<Button />}>Open a long name</SheetTrigger>
<SheetContent>
<SheetHeader>
<Avatar name={name} size="large" />
<SheetHeading>
<SheetTitle>{name}</SheetTitle>
<SheetDescription>王小明 Recruitment · Shanghai</SheetDescription>
</SheetHeading>
</SheetHeader>
<SheetBody>
{state === 'loading' && (
<SkeletonGroup label="Loading the application" className="flex flex-col gap-150 py-100">
<Skeleton width="60%" />
<Skeleton width="40%" />
<Skeleton width="50%" />
</SkeletonGroup>
)}
{state === 'error' && (
<SectionMessage appearance="error" title="Couldn’t load this application" actions={<Button onClick={load}>Retry</Button>}>
Check your connection and try again. The list behind still works.
</SectionMessage>
)}
{state === 'ready' && <p className="text-text-subtle">No interviews booked yet.</p>}
</SheetBody>
</SheetContent>
</Sheet>
);
}Motion
The sheet uses the Sheet motion. Press Replay to close and open it again.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetHeading, SheetTitle, SheetTrigger } from '@/ui/sheet';
// Replay closes the sheet, then opens it again, so the 180ms exit and the 250ms entry run back to back.
export default function SheetMotion() {
const [open, setOpen] = useState(false);
const replay = () => {
setOpen(false);
setTimeout(() => setOpen(true), 500);
};
return (
<Sheet open={open} onOpenChange={setOpen}>
<div className="flex gap-150">
<SheetTrigger render={<Button />}>Open Halcyon Bank</SheetTrigger>
<Button variant="ghost" onClick={replay}>
Replay
</Button>
</div>
<SheetContent>
<SheetHeader>
<SheetHeading>
<SheetTitle>Halcyon Bank</SheetTitle>
</SheetHeading>
</SheetHeader>
<SheetBody>Client since March 2024.</SheetBody>
</SheetContent>
</Sheet>
);
}