Skip to main content
NachUI

Sheet

A panel pinned to one edge of the screen, for filters, details and forms that should not take you off the page.

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Checkbox } from '@/components/ui/checkbox';
5import { Label } from '@/components/ui/label';
6import { Sheet } from '@/components/ui/sheet';
7
8const FILTERS = [
9 { id: 'open', label: 'Open pull requests', checked: true },
10 { id: 'mine', label: 'Assigned to me', checked: true },
11 { id: 'draft', label: 'Include drafts', checked: false },
12 { id: 'ci', label: 'Failing checks only', checked: false },
13];
14
15export function Default() {
16 return (
17 <Sheet>
18 <Sheet.Trigger asChild>
19 <Button variant="outline">Filters</Button>
20 </Sheet.Trigger>
21 <Sheet.Content>
22 <Sheet.Header>
23 <Sheet.Title>Filters</Sheet.Title>
24 <Sheet.Description>Narrow the list without leaving the page.</Sheet.Description>
25 </Sheet.Header>
26 <Sheet.Body className="flex flex-col gap-4">
27 {FILTERS.map((filter) => (
28 <div key={filter.id} className="flex items-center gap-3">
29 <Checkbox id={filter.id} defaultChecked={filter.checked} />
30 <Label htmlFor={filter.id} className="text-sm">
31 {filter.label}
32 </Label>
33 </div>
34 ))}
35 </Sheet.Body>
36 <Sheet.Footer>
37 <Sheet.Close asChild>
38 <Button variant="ghost" size="sm">
39 Reset
40 </Button>
41 </Sheet.Close>
42 <Sheet.Close asChild>
43 <Button size="sm">Apply</Button>
44 </Sheet.Close>
45 </Sheet.Footer>
46 </Sheet.Content>
47 </Sheet>
48 );
49}

Installation

pnpm dlx nachui add sheet

Anatomy

1import { Sheet } from '@/components/ui/sheet';
1<Sheet>
2 <Sheet.Trigger asChild>
3 <Button variant="outline">Filters</Button>
4 </Sheet.Trigger>
5 <Sheet.Content side="right" size="md">
6 <Sheet.Header>
7 <Sheet.Title>Filters</Sheet.Title>
8 <Sheet.Description>Narrow the list without leaving the page.</Sheet.Description>
9 </Sheet.Header>
10 <Sheet.Body>...</Sheet.Body>
11 <Sheet.Footer>
12 <Sheet.Close asChild>
13 <Button>Apply</Button>
14 </Sheet.Close>
15 </Sheet.Footer>
16 </Sheet.Content>
17</Sheet>
A sheet is flush with the edge it opens from and fills that edge end to end. It traps focus, closes on Escape and on the backdrop, locks page scroll and returns focus to the trigger when it goes.

Sheet or Drawer

They look related and they are, but they answer different questions. Drawer is the touch one: a floating island with a handle you drag to dismiss, at home on a phone. Sheet is the desktop one: a fixed side panel with no drag, sized in steps, meant for a filter rail, an edit form or a details view. If the content is something you fill in, reach for Sheet. If it is something you flick away, reach for Drawer.

Composition

Use the following composition to build a Sheet:
Sheet
├── Sheet.Trigger
└── Sheet.Content
├── Sheet.Header
│ ├── Sheet.Title
│ └── Sheet.Description
├── Sheet.Body
└── Sheet.Footer
└── Sheet.Close
Header, Body and Footer are layout: the body scrolls on its own and the footer sticks to the bottom, so long forms keep their actions in reach.

Sides

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Sheet, type SheetSide } from '@/components/ui/sheet';
5
6const SIDES: SheetSide[] = ['left', 'right', 'top', 'bottom'];
7
8export function Sides() {
9 return (
10 <div className="flex flex-wrap gap-2">
11 {SIDES.map((side) => (
12 <Sheet key={side}>
13 <Sheet.Trigger asChild>
14 <Button variant="outline" size="sm" className="capitalize">
15 {side}
16 </Button>
17 </Sheet.Trigger>
18 <Sheet.Content side={side}>
19 <Sheet.Header>
20 <Sheet.Title className="capitalize">{side}</Sheet.Title>
21 <Sheet.Description>A sheet anchored to the {side} edge.</Sheet.Description>
22 </Sheet.Header>
23 <Sheet.Body>
24 <p className="text-muted-foreground text-sm">
25 Press Escape, click the backdrop or use the close button to dismiss it.
26 </p>
27 </Sheet.Body>
28 </Sheet.Content>
29 </Sheet>
30 ))}
31 </div>
32 );
33}

Sizes

size is a width on the left and right, and a maximum height on the top and bottom. full covers the screen.
1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Sheet, type SheetSize } from '@/components/ui/sheet';
5
6const SIZES: SheetSize[] = ['sm', 'md', 'lg', 'xl', 'full'];
7
8export function Sizes() {
9 return (
10 <div className="flex flex-wrap gap-2">
11 {SIZES.map((size) => (
12 <Sheet key={size}>
13 <Sheet.Trigger asChild>
14 <Button variant="outline" size="sm">
15 {size}
16 </Button>
17 </Sheet.Trigger>
18 <Sheet.Content size={size}>
19 <Sheet.Header>
20 <Sheet.Title>Size {size}</Sheet.Title>
21 <Sheet.Description>Width on the sides, height on top and bottom.</Sheet.Description>
22 </Sheet.Header>
23 </Sheet.Content>
24 </Sheet>
25 ))}
26 </div>
27 );
28}

API Reference

Sheet

PropTypeDefaultDescription
defaultOpenbooleanfalseOpen state when uncontrolled
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Called when the open state changes

Sheet.Trigger

PropTypeDefaultDescription
asChildbooleanfalseRender the child as the trigger instead of a button
classNamestring-Additional CSS classes

Sheet.Content

PropTypeDefaultDescription
side'top' | 'bottom' | 'left' | 'right''right'Edge the sheet is pinned to
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'Width on the sides, height top and bottom
showClosebooleantrueShow the corner close button
closeLabelstring'Close'Accessible name of that button
classNamestring-Additional CSS classes

Sheet.Close

PropTypeDefaultDescription
asChildbooleanfalseRender the child as the closer instead of a button
classNamestring-Additional CSS classes

Sheet.Header / Sheet.Body / Sheet.Footer / Sheet.Title / Sheet.Description

PropTypeDefaultDescription
classNamestring-Additional CSS classes
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue