Skip to main content

Drawer

Slide-in context. Preserve page state. Seamless mobile UX.

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Drawer } from '@/components/ui/drawer';
5
6const activity = [
7 {
8 tone: 'bg-destructive-text',
9 title: 'Build failed on main',
10 detail: 'api-gateway, step "typecheck" exited with code 2.',
11 time: '4m ago',
12 },
13 {
14 tone: 'bg-success-text',
15 title: 'Deploy promoted to production',
16 detail: 'web v2.14.0 is now serving all traffic.',
17 time: '26m ago',
18 },
19 {
20 tone: 'bg-info-text',
21 title: 'Sofia Arriaga requested your review',
22 detail: 'Pull request #482, rate limits on checkout.',
23 time: '1h ago',
24 },
25 {
26 tone: 'bg-warning-text',
27 title: 'Postgres disk at 84%',
28 detail: 'db-primary in us-east-1 needs more storage this week.',
29 time: '3h ago',
30 },
31] as const;
32
33export function Default() {
34 return (
35 <Drawer>
36 <Drawer.Trigger variant="outline">Activity</Drawer.Trigger>
37 <Drawer.Content side="right">
38 <Drawer.Header>
39 <Drawer.Title>Activity</Drawer.Title>
40 <Drawer.Description>Everything that happened in Northwind today.</Drawer.Description>
41 </Drawer.Header>
42
43 <div className="flex flex-col gap-4">
44 {activity.map((event) => (
45 <div key={event.title} className="flex items-start gap-3">
46 <span
47 className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${event.tone}`}
48 aria-hidden="true"
49 />
50 <div className="min-w-0 flex-1">
51 <div className="flex items-baseline justify-between gap-3">
52 <p className="text-sm font-medium">{event.title}</p>
53 <span className="text-muted-foreground shrink-0 text-xs">{event.time}</span>
54 </div>
55 <p className="text-muted-foreground mt-0.5 text-xs">{event.detail}</p>
56 </div>
57 </div>
58 ))}
59 </div>
60
61 <div className="mt-6 flex items-center justify-between">
62 <Drawer.Close className="text-muted-foreground hover:text-foreground text-sm transition-colors">
63 Mark all as read
64 </Drawer.Close>
65 <Button size="sm">Open deploy log</Button>
66 </div>
67 </Drawer.Content>
68 </Drawer>
69 );
70}

Installation

pnpm dlx nachui add drawer

Anatomy

1import { Drawer } from '@/components/ui/drawer';
1<Drawer>
2 <Drawer.Trigger>
3 <Button>Open Drawer</Button>
4 </Drawer.Trigger>
5 <Drawer.Content>
6 <Drawer.Header>
7 <Drawer.Title>Drawer Title</Drawer.Title>
8 <Drawer.Description>This is a drawer component.</Drawer.Description>
9 </Drawer.Header>
10 <div>Your content here</div>
11 </Drawer.Content>
12</Drawer>

Composition

Use the following composition to build a Drawer:
Drawer
├── Drawer.Trigger
└── Drawer.Content
└── Drawer.Header
├── Drawer.Title
└── Drawer.Description

Features

  • Multiple sides - Slide from bottom, top, left, or right
  • Drag to close - Swipe to dismiss in any direction
  • Responsive - Adapts to different screen sizes
  • Smooth animations - Spring-based slide transitions
  • Focus trap - Keyboard navigation stays within the drawer
  • Body scroll lock - Prevents background scrolling when open

Variants

Positions

1'use client';
2
3import { Drawer } from '@/components/ui/drawer';
4
5const panels = [
6 {
7 side: 'top',
8 trigger: 'Release notes',
9 title: 'What changed in v2.14',
10 description: 'Shipped to production on Mar 14.',
11 items: [
12 'Deploy logs now stream while the build runs',
13 'Region picker moved into project settings',
14 'Fixed stale cache on custom domains',
15 ],
16 action: 'Got it',
17 },
18 {
19 side: 'right',
20 trigger: 'Filters',
21 title: 'Filter deploys',
22 description: '1,204 deploys in this project.',
23 items: ['Production only', 'Failed builds', 'Last 7 days'],
24 action: 'Apply filters',
25 },
26 {
27 side: 'bottom',
28 trigger: 'Quick actions',
29 title: 'Run a task',
30 description: 'Nothing here leaves the dashboard.',
31 items: ['Redeploy the latest commit', 'Roll back to v2.13.4', 'Clear the build cache'],
32 action: 'Close',
33 },
34 {
35 side: 'left',
36 trigger: 'Workspace',
37 title: 'Northwind',
38 description: 'Pro plan, 12 of 20 seats used.',
39 items: ['Projects', 'Deployments', 'Usage and billing'],
40 action: 'Close',
41 },
42] as const;
43
44export function Positions() {
45 return (
46 <div className="grid w-full max-w-md grid-cols-2 gap-4">
47 {panels.map((panel) => (
48 <Drawer key={panel.side}>
49 <Drawer.Trigger variant="outline" className="w-full justify-between gap-2">
50 {panel.trigger}
51 <span className="text-muted-foreground text-xs">{panel.side}</span>
52 </Drawer.Trigger>
53 <Drawer.Content side={panel.side}>
54 <Drawer.Header>
55 <Drawer.Title>{panel.title}</Drawer.Title>
56 <Drawer.Description>{panel.description}</Drawer.Description>
57 </Drawer.Header>
58 <ul className="text-muted-foreground flex flex-col gap-2 text-sm">
59 {panel.items.map((item) => (
60 <li key={item} className="border-border bg-card rounded-md border px-3 py-2">
61 {item}
62 </li>
63 ))}
64 </ul>
65 <div className="mt-6 flex justify-end">
66 <Drawer.Close className="text-muted-foreground hover:text-foreground text-sm transition-colors">
67 {panel.action}
68 </Drawer.Close>
69 </div>
70 </Drawer.Content>
71 </Drawer>
72 ))}
73 </div>
74 );
75}

Form

1'use client';
2
3import { Button } from '@/components/ui/button';
4import { Drawer } from '@/components/ui/drawer';
5import { Input } from '@/components/ui/input';
6import { Label } from '@/components/ui/label';
7import { Select } from '@/components/ui/select';
8
9export function Form() {
10 return (
11 <Drawer>
12 <Drawer.Trigger variant="outline">New API key</Drawer.Trigger>
13 <Drawer.Content side="bottom">
14 <Drawer.Header>
15 <Drawer.Title>Create an API key</Drawer.Title>
16 <Drawer.Description>
17 The secret is shown once, right after you create it. Store it in your CI provider before
18 you close this panel.
19 </Drawer.Description>
20 </Drawer.Header>
21
22 <div className="mx-auto grid w-full max-w-md gap-4 py-2">
23 <Input
24 id="key-name"
25 label="Key name"
26 defaultValue="ci-deploy-prod"
27 description="Shown in the audit log next to every request."
28 />
29 <div className="grid gap-4 sm:grid-cols-2">
30 <div className="grid gap-1.5">
31 <Label htmlFor="key-scope">Scope</Label>
32 <Select defaultValue="deploy">
33 <Select.Trigger id="key-scope" />
34 <Select.Content>
35 <Select.Item value="read">Read only</Select.Item>
36 <Select.Item value="deploy">Deploy and read logs</Select.Item>
37 <Select.Item value="admin">Full workspace access</Select.Item>
38 </Select.Content>
39 </Select>
40 </div>
41 <div className="grid gap-1.5">
42 <Label htmlFor="key-expiry">Expires</Label>
43 <Select defaultValue="90">
44 <Select.Trigger id="key-expiry" />
45 <Select.Content>
46 <Select.Item value="30">In 30 days</Select.Item>
47 <Select.Item value="90">In 90 days</Select.Item>
48 <Select.Item value="never">Never</Select.Item>
49 </Select.Content>
50 </Select>
51 </div>
52 </div>
53 </div>
54
55 <div className="mx-auto flex w-full max-w-md items-center justify-end gap-2">
56 <Drawer.Close className="text-muted-foreground hover:text-foreground text-sm transition-colors">
57 Cancel
58 </Drawer.Close>
59 <Button size="sm">Create key</Button>
60 </div>
61 </Drawer.Content>
62 </Drawer>
63 );
64}

API Reference

Drawer

PropTypeDefaultDescription
childrenReactNode-Drawer trigger and content
defaultOpenbooleanfalseInitial open state
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Callback when open changes

Drawer.Trigger

PropTypeDefaultDescription
childrenReactNode-Trigger button content
classNamestring-Additional CSS classes
variantButtonProps['variant']-Button variant
sizeButtonProps['size']-Button size

Drawer.Content

PropTypeDefaultDescription
side'bottom' | 'top' | 'left' | 'right''bottom'Side to slide from
showDragHandlebooleantrueShow drag handle for closing
classNamestring-Additional CSS classes

Drawer.Header

PropTypeDefaultDescription
classNamestring-Additional CSS classes

Drawer.Title

PropTypeDefaultDescription
classNamestring-Additional CSS classes

Drawer.Description

PropTypeDefaultDescription
classNamestring-Additional CSS classes

Drawer.Close

PropTypeDefaultDescription
childrenReactNode-Clickable content
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
Ctrl+I