@discourser/design-system 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +12 -4
  2. package/dist/styles.css +5126 -0
  3. package/guidelines/Guidelines.md +92 -41
  4. package/guidelines/components/accordion.md +732 -0
  5. package/guidelines/components/avatar.md +1015 -0
  6. package/guidelines/components/badge.md +728 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +671 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +1616 -0
  12. package/guidelines/components/heading.md +576 -0
  13. package/guidelines/components/icon-button.md +92 -37
  14. package/guidelines/components/input-addon.md +685 -0
  15. package/guidelines/components/input-group.md +830 -0
  16. package/guidelines/components/input.md +92 -37
  17. package/guidelines/components/popover.md +1271 -0
  18. package/guidelines/components/progress.md +836 -0
  19. package/guidelines/components/radio-group.md +852 -0
  20. package/guidelines/components/select.md +1662 -0
  21. package/guidelines/components/skeleton.md +802 -0
  22. package/guidelines/components/slider.md +911 -0
  23. package/guidelines/components/spinner.md +783 -0
  24. package/guidelines/components/switch.md +105 -38
  25. package/guidelines/components/tabs.md +1488 -0
  26. package/guidelines/components/textarea.md +495 -0
  27. package/guidelines/components/toast.md +784 -0
  28. package/guidelines/components/tooltip.md +912 -0
  29. package/guidelines/design-tokens/colors.md +309 -72
  30. package/guidelines/design-tokens/elevation.md +615 -45
  31. package/guidelines/design-tokens/spacing.md +654 -74
  32. package/guidelines/design-tokens/typography.md +432 -50
  33. package/guidelines/overview-components.md +60 -8
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -0,0 +1,1616 @@
1
+ # Drawer
2
+
3
+ **Purpose:** A panel that slides in from the edge of the screen, used for navigation, forms, or additional content without leaving the current context. Built on Ark UI's Dialog primitive with specialized styling for edge-anchored panels.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use Drawer when you need to **display secondary content, navigation, or forms that slide in from the screen edge** without interrupting the user's context.
8
+
9
+ ### Decision Tree
10
+
11
+ | Scenario | Use Drawer? | Alternative | Reasoning |
12
+ | ------------------------------------ | ----------- | ------------------ | ------------------------------------------------- |
13
+ | Navigation menu (mobile) | ✅ Yes | - | Drawer slides from side, perfect for mobile menus |
14
+ | Displaying filters or settings panel | ✅ Yes | - | Keeps main content visible while showing options |
15
+ | Shopping cart or preview panel | ✅ Yes | - | Non-modal context, easy to dismiss |
16
+ | Critical confirmations or alerts | ❌ No | Dialog | Dialog is centered and demands more attention |
17
+ | Small contextual information | ❌ No | Popover or Tooltip | Drawer is too heavy for brief hints |
18
+ | Multi-step form as primary content | ❌ No | Full page | Complex forms deserve dedicated space |
19
+
20
+ ### Component Comparison
21
+
22
+ ```typescript
23
+ // ✅ Drawer - Navigation menu from side
24
+ <Drawer.Root placement="start" size="sm">
25
+ <Drawer.Trigger asChild>
26
+ <Button leftIcon={<MenuIcon />}>Menu</Button>
27
+ </Drawer.Trigger>
28
+ <Drawer.Backdrop />
29
+ <Drawer.Positioner>
30
+ <Drawer.Content>
31
+ <Drawer.Header>
32
+ <Drawer.Title>Navigation</Drawer.Title>
33
+ <Drawer.CloseTrigger asChild>
34
+ <IconButton aria-label="Close"><XIcon /></IconButton>
35
+ </Drawer.CloseTrigger>
36
+ </Drawer.Header>
37
+ <Drawer.Body>
38
+ <nav>
39
+ <a href="/home">Home</a>
40
+ <a href="/about">About</a>
41
+ </nav>
42
+ </Drawer.Body>
43
+ </Drawer.Content>
44
+ </Drawer.Positioner>
45
+ </Drawer.Root>
46
+
47
+ // ❌ Don't use Drawer for critical alerts - Use Dialog
48
+ <Drawer.Root placement="bottom">
49
+ <Drawer.Content>
50
+ <Drawer.Title>Delete Account?</Drawer.Title>
51
+ <Drawer.Body>This action cannot be undone.</Drawer.Body>
52
+ {/* Critical actions need centered Dialog */}
53
+ </Drawer.Content>
54
+ </Drawer.Root>
55
+
56
+ // ✅ Better: Use Dialog for critical confirmations
57
+ <Dialog.Root>
58
+ <Dialog.Backdrop />
59
+ <Dialog.Positioner>
60
+ <Dialog.Content>
61
+ <Dialog.Title>Delete Account?</Dialog.Title>
62
+ <Dialog.Description>
63
+ This action cannot be undone.
64
+ </Dialog.Description>
65
+ <Dialog.Footer>
66
+ <Button variant="outlined">Cancel</Button>
67
+ <Button colorPalette="error">Delete</Button>
68
+ </Dialog.Footer>
69
+ </Dialog.Content>
70
+ </Dialog.Positioner>
71
+ </Dialog.Root>
72
+
73
+ // ❌ Don't use Drawer for small hints - Use Popover
74
+ <Drawer.Root size="xs">
75
+ <Drawer.Content>
76
+ <Drawer.Body>
77
+ Click here for more info
78
+ </Drawer.Body>
79
+ </Drawer.Content>
80
+ </Drawer.Root>
81
+
82
+ // ✅ Better: Use Popover for contextual info
83
+ <Popover.Root>
84
+ <Popover.Trigger asChild>
85
+ <Button>Info</Button>
86
+ </Popover.Trigger>
87
+ <Popover.Positioner>
88
+ <Popover.Content>
89
+ <Popover.Title>Quick Info</Popover.Title>
90
+ <Popover.Description>Click here for more info</Popover.Description>
91
+ </Popover.Content>
92
+ </Popover.Positioner>
93
+ </Popover.Root>
94
+ ```
95
+
96
+ ## Import
97
+
98
+ ```typescript
99
+ import { Drawer } from '@discourser/design-system';
100
+ ```
101
+
102
+ ## Component Structure
103
+
104
+ Drawer is a compound component built using Ark UI's Dialog primitive. It consists of several parts that work together:
105
+
106
+ ### Core Components
107
+
108
+ | Component | Purpose | Required |
109
+ | --------------------- | -------------------------------------- | ----------- |
110
+ | `Drawer.Root` | Main container and state manager | Yes |
111
+ | `Drawer.Trigger` | Element that opens the drawer | Yes |
112
+ | `Drawer.Backdrop` | Semi-transparent overlay behind drawer | Recommended |
113
+ | `Drawer.Positioner` | Positions drawer at screen edge | Yes |
114
+ | `Drawer.Content` | Main drawer panel | Yes |
115
+ | `Drawer.Title` | Drawer heading (for accessibility) | Yes |
116
+ | `Drawer.Description` | Drawer description (for accessibility) | Recommended |
117
+ | `Drawer.CloseTrigger` | Button to close drawer | Recommended |
118
+
119
+ ### Layout Components
120
+
121
+ | Component | Purpose | Usage |
122
+ | --------------- | -------------------------------------- | ----------- |
123
+ | `Drawer.Header` | Top section for title and close button | Recommended |
124
+ | `Drawer.Body` | Main scrollable content area | Recommended |
125
+ | `Drawer.Footer` | Bottom section for actions | Optional |
126
+
127
+ ### Context
128
+
129
+ | Component | Purpose |
130
+ | ---------------- | ------------------------------------ |
131
+ | `Drawer.Context` | Access drawer state programmatically |
132
+
133
+ ## Variants
134
+
135
+ ### Placement
136
+
137
+ Controls which edge of the screen the drawer slides from:
138
+
139
+ | Placement | Behavior | Usage |
140
+ | --------- | ------------------------------- | --------------------------------------- |
141
+ | `start` | Slides from left (right in RTL) | Navigation menus, filters |
142
+ | `end` | Slides from right (left in RTL) | Settings, detail panels, shopping carts |
143
+ | `top` | Slides from top | Notifications, announcements |
144
+ | `bottom` | Slides from bottom | Mobile sheets, quick actions |
145
+
146
+ **Default:** `end`
147
+
148
+ **Animation Details:**
149
+
150
+ - `start/end`: Slides horizontally with fade, duration: slowest (open), normal (close)
151
+ - `top/bottom`: Slides vertically with fade, duration: slowest (open), normal (close)
152
+ - Uses emphasized easing curves for smooth, natural motion
153
+
154
+ ### Size
155
+
156
+ Controls the width (start/end) or height (top/bottom) of the drawer:
157
+
158
+ | Size | Dimension | Usage |
159
+ | ------ | ------------- | ------------------------------------- |
160
+ | `xs` | 320px (20rem) | Minimal content, mobile-first |
161
+ | `sm` | 384px (24rem) | Standard navigation, compact forms |
162
+ | `md` | 448px (28rem) | Detailed forms, rich content |
163
+ | `lg` | 512px (32rem) | Complex panels, multi-section content |
164
+ | `xl` | 576px (36rem) | Full-featured panels, dashboards |
165
+ | `full` | 100vw/100dvh | Fullscreen mode, mobile takeover |
166
+
167
+ **Default:** `sm`
168
+
169
+ **Note:** For `top` and `bottom` placements, size controls height. For `start` and `end`, it controls width.
170
+
171
+ ## Props
172
+
173
+ ### Root Props
174
+
175
+ | Prop | Type | Default | Description |
176
+ | ------------------------ | ------------------------------------------------ | -------------- | ------------------------------------ |
177
+ | `placement` | `'start' \| 'end' \| 'top' \| 'bottom'` | `'end'` | Screen edge to slide from |
178
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'sm'` | Width/height of drawer |
179
+ | `open` | `boolean` | - | Controlled open state |
180
+ | `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
181
+ | `onOpenChange` | `(details: { open: boolean }) => void` | - | Callback when open state changes |
182
+ | `closeOnInteractOutside` | `boolean` | `true` | Close when clicking backdrop |
183
+ | `closeOnEscapeKeyDown` | `boolean` | `true` | Close when pressing Escape |
184
+ | `preventScroll` | `boolean` | `true` | Prevent body scroll when open |
185
+ | `unmountOnExit` | `boolean` | `true` | Remove from DOM when closed |
186
+ | `lazyMount` | `boolean` | `true` | Mount content only when first opened |
187
+ | `modal` | `boolean` | `true` | Trap focus within drawer |
188
+ | `id` | `string` | auto-generated | Unique ID for accessibility |
189
+
190
+ ### Content Props
191
+
192
+ All compound components accept standard HTML attributes plus styling props from Panda CSS.
193
+
194
+ ## Examples
195
+
196
+ ### Basic Usage
197
+
198
+ ```typescript
199
+ import { Drawer, Button } from '@discourser/design-system';
200
+ import { XIcon } from 'your-icon-library';
201
+
202
+ function BasicDrawer() {
203
+ return (
204
+ <Drawer.Root>
205
+ <Drawer.Trigger asChild>
206
+ <Button>Open Drawer</Button>
207
+ </Drawer.Trigger>
208
+
209
+ <Drawer.Backdrop />
210
+
211
+ <Drawer.Positioner>
212
+ <Drawer.Content>
213
+ <Drawer.Header>
214
+ <Drawer.Title>Drawer Title</Drawer.Title>
215
+ <Drawer.Description>
216
+ This is a description of what the drawer contains
217
+ </Drawer.Description>
218
+ <Drawer.CloseTrigger asChild>
219
+ <Button variant="text" size="sm">
220
+ <XIcon />
221
+ </Button>
222
+ </Drawer.CloseTrigger>
223
+ </Drawer.Header>
224
+
225
+ <Drawer.Body>
226
+ <p>Main content goes here</p>
227
+ </Drawer.Body>
228
+
229
+ <Drawer.Footer>
230
+ <Drawer.CloseTrigger asChild>
231
+ <Button variant="outlined">Cancel</Button>
232
+ </Drawer.CloseTrigger>
233
+ <Button>Save</Button>
234
+ </Drawer.Footer>
235
+ </Drawer.Content>
236
+ </Drawer.Positioner>
237
+ </Drawer.Root>
238
+ );
239
+ }
240
+ ```
241
+
242
+ ### Controlled Drawer
243
+
244
+ ```typescript
245
+ import { useState } from 'react';
246
+ import { Drawer, Button } from '@discourser/design-system';
247
+
248
+ function ControlledDrawer() {
249
+ const [open, setOpen] = useState(false);
250
+
251
+ const handleSave = () => {
252
+ // Save logic here
253
+ setOpen(false);
254
+ };
255
+
256
+ return (
257
+ <>
258
+ <Button onClick={() => setOpen(true)}>Open Settings</Button>
259
+
260
+ <Drawer.Root open={open} onOpenChange={(details) => setOpen(details.open)}>
261
+ <Drawer.Backdrop />
262
+ <Drawer.Positioner>
263
+ <Drawer.Content>
264
+ <Drawer.Header>
265
+ <Drawer.Title>Settings</Drawer.Title>
266
+ <Drawer.CloseTrigger asChild>
267
+ <Button variant="text" size="sm">
268
+ <XIcon />
269
+ </Button>
270
+ </Drawer.CloseTrigger>
271
+ </Drawer.Header>
272
+
273
+ <Drawer.Body>
274
+ {/* Settings form */}
275
+ </Drawer.Body>
276
+
277
+ <Drawer.Footer>
278
+ <Button variant="outlined" onClick={() => setOpen(false)}>
279
+ Cancel
280
+ </Button>
281
+ <Button onClick={handleSave}>Save Changes</Button>
282
+ </Drawer.Footer>
283
+ </Drawer.Content>
284
+ </Drawer.Positioner>
285
+ </Drawer.Root>
286
+ </>
287
+ );
288
+ }
289
+ ```
290
+
291
+ ### Different Placements
292
+
293
+ ```typescript
294
+ // Navigation drawer (left side)
295
+ <Drawer.Root placement="start" size="sm">
296
+ <Drawer.Trigger asChild>
297
+ <Button leftIcon={<MenuIcon />}>Menu</Button>
298
+ </Drawer.Trigger>
299
+ <Drawer.Backdrop />
300
+ <Drawer.Positioner>
301
+ <Drawer.Content>
302
+ <Drawer.Header>
303
+ <Drawer.Title>Navigation</Drawer.Title>
304
+ </Drawer.Header>
305
+ <Drawer.Body>
306
+ <nav>
307
+ <a href="/home">Home</a>
308
+ <a href="/about">About</a>
309
+ <a href="/contact">Contact</a>
310
+ </nav>
311
+ </Drawer.Body>
312
+ </Drawer.Content>
313
+ </Drawer.Positioner>
314
+ </Drawer.Root>
315
+
316
+ // Shopping cart drawer (right side)
317
+ <Drawer.Root placement="end" size="md">
318
+ <Drawer.Trigger asChild>
319
+ <Button rightIcon={<CartIcon />}>Cart (3)</Button>
320
+ </Drawer.Trigger>
321
+ <Drawer.Backdrop />
322
+ <Drawer.Positioner>
323
+ <Drawer.Content>
324
+ <Drawer.Header>
325
+ <Drawer.Title>Shopping Cart</Drawer.Title>
326
+ </Drawer.Header>
327
+ <Drawer.Body>
328
+ {/* Cart items */}
329
+ </Drawer.Body>
330
+ <Drawer.Footer>
331
+ <Button variant="filled">Checkout</Button>
332
+ </Drawer.Footer>
333
+ </Drawer.Content>
334
+ </Drawer.Positioner>
335
+ </Drawer.Root>
336
+
337
+ // Mobile bottom sheet
338
+ <Drawer.Root placement="bottom" size="md">
339
+ <Drawer.Trigger asChild>
340
+ <Button>Share</Button>
341
+ </Drawer.Trigger>
342
+ <Drawer.Backdrop />
343
+ <Drawer.Positioner>
344
+ <Drawer.Content>
345
+ <Drawer.Header>
346
+ <Drawer.Title>Share Options</Drawer.Title>
347
+ </Drawer.Header>
348
+ <Drawer.Body>
349
+ {/* Share options */}
350
+ </Drawer.Body>
351
+ </Drawer.Content>
352
+ </Drawer.Positioner>
353
+ </Drawer.Root>
354
+ ```
355
+
356
+ ### Different Sizes
357
+
358
+ ```typescript
359
+ // Compact drawer for filters
360
+ <Drawer.Root size="xs">
361
+ <Drawer.Trigger asChild>
362
+ <Button>Filters</Button>
363
+ </Drawer.Trigger>
364
+ <Drawer.Backdrop />
365
+ <Drawer.Positioner>
366
+ <Drawer.Content>
367
+ <Drawer.Header>
368
+ <Drawer.Title>Filter Results</Drawer.Title>
369
+ </Drawer.Header>
370
+ <Drawer.Body>
371
+ {/* Compact filter options */}
372
+ </Drawer.Body>
373
+ </Drawer.Content>
374
+ </Drawer.Positioner>
375
+ </Drawer.Root>
376
+
377
+ // Large drawer for detailed content
378
+ <Drawer.Root size="lg">
379
+ <Drawer.Trigger asChild>
380
+ <Button>View Details</Button>
381
+ </Drawer.Trigger>
382
+ <Drawer.Backdrop />
383
+ <Drawer.Positioner>
384
+ <Drawer.Content>
385
+ <Drawer.Header>
386
+ <Drawer.Title>Product Details</Drawer.Title>
387
+ </Drawer.Header>
388
+ <Drawer.Body>
389
+ {/* Rich content with images, descriptions, etc. */}
390
+ </Drawer.Body>
391
+ </Drawer.Content>
392
+ </Drawer.Positioner>
393
+ </Drawer.Root>
394
+
395
+ // Fullscreen drawer for mobile
396
+ <Drawer.Root size="full">
397
+ <Drawer.Trigger asChild>
398
+ <Button>Edit Profile</Button>
399
+ </Drawer.Trigger>
400
+ <Drawer.Positioner>
401
+ <Drawer.Content>
402
+ <Drawer.Header>
403
+ <Drawer.Title>Edit Profile</Drawer.Title>
404
+ </Drawer.Header>
405
+ <Drawer.Body>
406
+ {/* Full editing interface */}
407
+ </Drawer.Body>
408
+ <Drawer.Footer>
409
+ <Button>Save Changes</Button>
410
+ </Drawer.Footer>
411
+ </Drawer.Content>
412
+ </Drawer.Positioner>
413
+ </Drawer.Root>
414
+ ```
415
+
416
+ ### Form in Drawer
417
+
418
+ ```typescript
419
+ import { Drawer, Button, Input, Textarea } from '@discourser/design-system';
420
+ import { useState } from 'react';
421
+
422
+ function FormDrawer() {
423
+ const [formData, setFormData] = useState({ name: '', email: '', message: '' });
424
+
425
+ const handleSubmit = (e: React.FormEvent) => {
426
+ e.preventDefault();
427
+ // Submit form
428
+ console.log('Form submitted:', formData);
429
+ };
430
+
431
+ return (
432
+ <Drawer.Root placement="end" size="md">
433
+ <Drawer.Trigger asChild>
434
+ <Button>Contact Us</Button>
435
+ </Drawer.Trigger>
436
+
437
+ <Drawer.Backdrop />
438
+
439
+ <Drawer.Positioner>
440
+ <Drawer.Content>
441
+ <form onSubmit={handleSubmit}>
442
+ <Drawer.Header>
443
+ <Drawer.Title>Contact Form</Drawer.Title>
444
+ <Drawer.Description>
445
+ Send us a message and we'll get back to you soon
446
+ </Drawer.Description>
447
+ <Drawer.CloseTrigger asChild>
448
+ <Button variant="text" size="sm" type="button">
449
+ <XIcon />
450
+ </Button>
451
+ </Drawer.CloseTrigger>
452
+ </Drawer.Header>
453
+
454
+ <Drawer.Body>
455
+ <Input
456
+ label="Name"
457
+ value={formData.name}
458
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
459
+ required
460
+ />
461
+ <Input
462
+ label="Email"
463
+ type="email"
464
+ value={formData.email}
465
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
466
+ required
467
+ />
468
+ <Textarea
469
+ label="Message"
470
+ value={formData.message}
471
+ onChange={(e) => setFormData({ ...formData, message: e.target.value })}
472
+ rows={5}
473
+ required
474
+ />
475
+ </Drawer.Body>
476
+
477
+ <Drawer.Footer>
478
+ <Drawer.CloseTrigger asChild>
479
+ <Button variant="outlined" type="button">Cancel</Button>
480
+ </Drawer.CloseTrigger>
481
+ <Button type="submit">Send Message</Button>
482
+ </Drawer.Footer>
483
+ </form>
484
+ </Drawer.Content>
485
+ </Drawer.Positioner>
486
+ </Drawer.Root>
487
+ );
488
+ }
489
+ ```
490
+
491
+ ### Navigation Menu Drawer
492
+
493
+ ```typescript
494
+ import { Drawer, Button, IconButton } from '@discourser/design-system';
495
+ import { MenuIcon, HomeIcon, SettingsIcon, UserIcon, XIcon } from 'your-icon-library';
496
+
497
+ function NavigationDrawer() {
498
+ const menuItems = [
499
+ { icon: <HomeIcon />, label: 'Home', href: '/' },
500
+ { icon: <UserIcon />, label: 'Profile', href: '/profile' },
501
+ { icon: <SettingsIcon />, label: 'Settings', href: '/settings' },
502
+ ];
503
+
504
+ return (
505
+ <Drawer.Root placement="start" size="sm">
506
+ <Drawer.Trigger asChild>
507
+ <IconButton aria-label="Open menu">
508
+ <MenuIcon />
509
+ </IconButton>
510
+ </Drawer.Trigger>
511
+
512
+ <Drawer.Backdrop />
513
+
514
+ <Drawer.Positioner>
515
+ <Drawer.Content>
516
+ <Drawer.Header>
517
+ <Drawer.Title>Menu</Drawer.Title>
518
+ <Drawer.CloseTrigger asChild>
519
+ <IconButton aria-label="Close menu" variant="text" size="sm">
520
+ <XIcon />
521
+ </IconButton>
522
+ </Drawer.CloseTrigger>
523
+ </Drawer.Header>
524
+
525
+ <Drawer.Body>
526
+ <nav className={css({ display: 'flex', flexDirection: 'column', gap: '2' })}>
527
+ {menuItems.map((item) => (
528
+ <a
529
+ key={item.href}
530
+ href={item.href}
531
+ className={css({
532
+ display: 'flex',
533
+ alignItems: 'center',
534
+ gap: '3',
535
+ p: '3',
536
+ borderRadius: 'md',
537
+ color: 'fg.default',
538
+ textDecoration: 'none',
539
+ _hover: { bg: 'gray.a3' },
540
+ })}
541
+ >
542
+ {item.icon}
543
+ <span>{item.label}</span>
544
+ </a>
545
+ ))}
546
+ </nav>
547
+ </Drawer.Body>
548
+ </Drawer.Content>
549
+ </Drawer.Positioner>
550
+ </Drawer.Root>
551
+ );
552
+ }
553
+ ```
554
+
555
+ ### Using Context
556
+
557
+ ```typescript
558
+ import { Drawer } from '@discourser/design-system';
559
+
560
+ function DrawerWithContext() {
561
+ return (
562
+ <Drawer.Root>
563
+ <Drawer.Trigger asChild>
564
+ <Button>Open</Button>
565
+ </Drawer.Trigger>
566
+
567
+ <Drawer.Backdrop />
568
+
569
+ <Drawer.Positioner>
570
+ <Drawer.Content>
571
+ <Drawer.Header>
572
+ <Drawer.Title>Custom Content</Drawer.Title>
573
+ </Drawer.Header>
574
+
575
+ <Drawer.Body>
576
+ <Drawer.Context>
577
+ {(context) => (
578
+ <div>
579
+ <p>Drawer is {context.open ? 'open' : 'closed'}</p>
580
+ <Button onClick={() => context.setOpen(false)}>
581
+ Close Programmatically
582
+ </Button>
583
+ </div>
584
+ )}
585
+ </Drawer.Context>
586
+ </Drawer.Body>
587
+ </Drawer.Content>
588
+ </Drawer.Positioner>
589
+ </Drawer.Root>
590
+ );
591
+ }
592
+ ```
593
+
594
+ ## Common Patterns
595
+
596
+ ### Confirmation Before Close
597
+
598
+ ```typescript
599
+ function ConfirmCloseDrawer() {
600
+ const [hasChanges, setHasChanges] = useState(false);
601
+ const [showConfirm, setShowConfirm] = useState(false);
602
+
603
+ const handleInteractOutside = (e: Event) => {
604
+ if (hasChanges) {
605
+ e.preventDefault();
606
+ setShowConfirm(true);
607
+ }
608
+ };
609
+
610
+ return (
611
+ <>
612
+ <Drawer.Root onInteractOutside={handleInteractOutside}>
613
+ <Drawer.Trigger asChild>
614
+ <Button>Edit Settings</Button>
615
+ </Drawer.Trigger>
616
+
617
+ <Drawer.Backdrop />
618
+
619
+ <Drawer.Positioner>
620
+ <Drawer.Content>
621
+ <Drawer.Header>
622
+ <Drawer.Title>Settings</Drawer.Title>
623
+ </Drawer.Header>
624
+
625
+ <Drawer.Body>
626
+ <Input onChange={() => setHasChanges(true)} />
627
+ </Drawer.Body>
628
+
629
+ <Drawer.Footer>
630
+ <Button>Save</Button>
631
+ </Drawer.Footer>
632
+ </Drawer.Content>
633
+ </Drawer.Positioner>
634
+ </Drawer.Root>
635
+
636
+ {showConfirm && (
637
+ <Dialog.Root open onOpenChange={(e) => setShowConfirm(e.open)}>
638
+ <Dialog.Content>
639
+ <Dialog.Title>Unsaved Changes</Dialog.Title>
640
+ <Dialog.Description>
641
+ You have unsaved changes. Are you sure you want to close?
642
+ </Dialog.Description>
643
+ <Dialog.Footer>
644
+ <Button variant="outlined" onClick={() => setShowConfirm(false)}>
645
+ Cancel
646
+ </Button>
647
+ <Button onClick={() => {
648
+ setShowConfirm(false);
649
+ setHasChanges(false);
650
+ }}>
651
+ Discard Changes
652
+ </Button>
653
+ </Dialog.Footer>
654
+ </Dialog.Content>
655
+ </Dialog.Root>
656
+ )}
657
+ </>
658
+ );
659
+ }
660
+ ```
661
+
662
+ ### Multi-Step Drawer
663
+
664
+ ```typescript
665
+ function MultiStepDrawer() {
666
+ const [step, setStep] = useState(1);
667
+
668
+ return (
669
+ <Drawer.Root size="md">
670
+ <Drawer.Trigger asChild>
671
+ <Button>Start Wizard</Button>
672
+ </Drawer.Trigger>
673
+
674
+ <Drawer.Backdrop />
675
+
676
+ <Drawer.Positioner>
677
+ <Drawer.Content>
678
+ <Drawer.Header>
679
+ <Drawer.Title>Setup Wizard - Step {step} of 3</Drawer.Title>
680
+ <Drawer.CloseTrigger asChild>
681
+ <IconButton aria-label="Close" variant="text" size="sm">
682
+ <XIcon />
683
+ </IconButton>
684
+ </Drawer.CloseTrigger>
685
+ </Drawer.Header>
686
+
687
+ <Drawer.Body>
688
+ {step === 1 && <div>Step 1 content</div>}
689
+ {step === 2 && <div>Step 2 content</div>}
690
+ {step === 3 && <div>Step 3 content</div>}
691
+ </Drawer.Body>
692
+
693
+ <Drawer.Footer>
694
+ {step > 1 && (
695
+ <Button variant="outlined" onClick={() => setStep(step - 1)}>
696
+ Back
697
+ </Button>
698
+ )}
699
+ {step < 3 ? (
700
+ <Button onClick={() => setStep(step + 1)}>Next</Button>
701
+ ) : (
702
+ <Button>Finish</Button>
703
+ )}
704
+ </Drawer.Footer>
705
+ </Drawer.Content>
706
+ </Drawer.Positioner>
707
+ </Drawer.Root>
708
+ );
709
+ }
710
+ ```
711
+
712
+ ### Responsive Drawer
713
+
714
+ ```typescript
715
+ function ResponsiveDrawer() {
716
+ return (
717
+ <Drawer.Root
718
+ placement={{ base: 'bottom', md: 'end' }}
719
+ size={{ base: 'md', md: 'sm' }}
720
+ >
721
+ <Drawer.Trigger asChild>
722
+ <Button>Open Filters</Button>
723
+ </Drawer.Trigger>
724
+
725
+ <Drawer.Backdrop />
726
+
727
+ <Drawer.Positioner>
728
+ <Drawer.Content>
729
+ <Drawer.Header>
730
+ <Drawer.Title>Filters</Drawer.Title>
731
+ <Drawer.CloseTrigger asChild>
732
+ <IconButton aria-label="Close" variant="text" size="sm">
733
+ <XIcon />
734
+ </IconButton>
735
+ </Drawer.CloseTrigger>
736
+ </Drawer.Header>
737
+
738
+ <Drawer.Body>
739
+ {/* Filter options - layout adapts to placement */}
740
+ </Drawer.Body>
741
+
742
+ <Drawer.Footer>
743
+ <Button variant="outlined">Clear</Button>
744
+ <Button>Apply Filters</Button>
745
+ </Drawer.Footer>
746
+ </Drawer.Content>
747
+ </Drawer.Positioner>
748
+ </Drawer.Root>
749
+ );
750
+ }
751
+ ```
752
+
753
+ ## Edge Cases
754
+
755
+ This section covers common edge cases and how to handle them properly.
756
+
757
+ ### Stacked Drawers - Multiple Drawers Open
758
+
759
+ **Scenario:** Multiple drawers need to be open simultaneously, such as a navigation drawer with an overlay drawer for details.
760
+
761
+ **Solution:**
762
+
763
+ ```typescript
764
+ const [navDrawerOpen, setNavDrawerOpen] = useState(false);
765
+ const [detailsDrawerOpen, setDetailsDrawerOpen] = useState(false);
766
+
767
+ // Track z-index levels for proper stacking
768
+ const navDrawerZIndex = 1000;
769
+ const detailsDrawerZIndex = 1100;
770
+
771
+ <>
772
+ {/* Primary navigation drawer from left */}
773
+ <Drawer.Root
774
+ placement="start"
775
+ size="sm"
776
+ open={navDrawerOpen}
777
+ onOpenChange={(details) => setNavDrawerOpen(details.open)}
778
+ >
779
+ <Drawer.Trigger asChild>
780
+ <Button leftIcon={<MenuIcon />}>Menu</Button>
781
+ </Drawer.Trigger>
782
+
783
+ <Drawer.Backdrop style={{ zIndex: navDrawerZIndex }} />
784
+
785
+ <Drawer.Positioner style={{ zIndex: navDrawerZIndex + 1 }}>
786
+ <Drawer.Content>
787
+ <Drawer.Header>
788
+ <Drawer.Title>Navigation</Drawer.Title>
789
+ <Drawer.CloseTrigger asChild>
790
+ <IconButton aria-label="Close menu" variant="text" size="sm">
791
+ <XIcon />
792
+ </IconButton>
793
+ </Drawer.CloseTrigger>
794
+ </Drawer.Header>
795
+
796
+ <Drawer.Body>
797
+ <nav className={css({ display: 'flex', flexDirection: 'column', gap: '2' })}>
798
+ <a href="/">Home</a>
799
+ <a href="/about">About</a>
800
+ <button
801
+ onClick={() => setDetailsDrawerOpen(true)}
802
+ className={css({ textAlign: 'left', p: '2' })}
803
+ >
804
+ View Details
805
+ </button>
806
+ </nav>
807
+ </Drawer.Body>
808
+ </Drawer.Content>
809
+ </Drawer.Positioner>
810
+ </Drawer.Root>
811
+
812
+ {/* Secondary details drawer from right - higher z-index */}
813
+ <Drawer.Root
814
+ placement="end"
815
+ size="md"
816
+ open={detailsDrawerOpen}
817
+ onOpenChange={(details) => setDetailsDrawerOpen(details.open)}
818
+ >
819
+ <Drawer.Backdrop style={{ zIndex: detailsDrawerZIndex }} />
820
+
821
+ <Drawer.Positioner style={{ zIndex: detailsDrawerZIndex + 1 }}>
822
+ <Drawer.Content>
823
+ <Drawer.Header>
824
+ <Drawer.Title>Details</Drawer.Title>
825
+ <Drawer.CloseTrigger asChild>
826
+ <IconButton aria-label="Close details" variant="text" size="sm">
827
+ <XIcon />
828
+ </IconButton>
829
+ </Drawer.CloseTrigger>
830
+ </Drawer.Header>
831
+
832
+ <Drawer.Body>
833
+ <div className={css({ p: '4' })}>
834
+ <p>This drawer appears on top of the navigation drawer.</p>
835
+ <p className={css({ mt: '2', fontSize: 'sm', color: 'fg.muted' })}>
836
+ Both drawers remain independently interactive.
837
+ </p>
838
+ </div>
839
+ </Drawer.Body>
840
+
841
+ <Drawer.Footer>
842
+ <Button variant="outlined" onClick={() => setDetailsDrawerOpen(false)}>
843
+ Close
844
+ </Button>
845
+ </Drawer.Footer>
846
+ </Drawer.Content>
847
+ </Drawer.Positioner>
848
+ </Drawer.Root>
849
+ </>
850
+ ```
851
+
852
+ **Best practices:**
853
+
854
+ - Limit stacked drawers to two maximum to avoid confusion
855
+ - Use different placements for stacked drawers (e.g., start + end)
856
+ - Ensure proper z-index stacking so upper drawers overlay lower ones
857
+ - Make each drawer independently closable
858
+ - Consider closing lower drawers when opening upper ones for simplicity
859
+
860
+ ---
861
+
862
+ ### Mobile Considerations - Full-Screen Behavior
863
+
864
+ **Scenario:** Drawers should adapt to mobile screens, potentially becoming full-screen to maximize usable space.
865
+
866
+ **Solution:**
867
+
868
+ ```typescript
869
+ import { useMediaQuery } from '@/hooks/useMediaQuery';
870
+
871
+ const [open, setOpen] = useState(false);
872
+ const isMobile = useMediaQuery('(max-width: 768px)');
873
+
874
+ <Drawer.Root
875
+ placement={isMobile ? 'bottom' : 'end'}
876
+ size={isMobile ? 'full' : 'md'}
877
+ open={open}
878
+ onOpenChange={(details) => setOpen(details.open)}
879
+ >
880
+ <Drawer.Trigger asChild>
881
+ <Button>Open Filters</Button>
882
+ </Drawer.Trigger>
883
+
884
+ <Drawer.Backdrop />
885
+
886
+ <Drawer.Positioner>
887
+ <Drawer.Content
888
+ className={css({
889
+ // On mobile, add safe area padding for notched devices
890
+ paddingBottom: isMobile ? 'env(safe-area-inset-bottom)' : undefined,
891
+ })}
892
+ >
893
+ <Drawer.Header>
894
+ <Drawer.Title>Filter Options</Drawer.Title>
895
+ <Drawer.CloseTrigger asChild>
896
+ <IconButton aria-label="Close filters" variant="text" size="sm">
897
+ <XIcon />
898
+ </IconButton>
899
+ </Drawer.CloseTrigger>
900
+ </Drawer.Header>
901
+
902
+ <Drawer.Body>
903
+ {/* Filter options */}
904
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: '4' })}>
905
+ <div>
906
+ <label className={css({ display: 'block', mb: '2' })}>Price Range</label>
907
+ <input type="range" min="0" max="1000" />
908
+ </div>
909
+ <div>
910
+ <label className={css({ display: 'block', mb: '2' })}>Category</label>
911
+ <Select.Root items={['All', 'Electronics', 'Clothing']}>
912
+ <Select.Control>
913
+ <Select.Trigger>
914
+ <Select.ValueText placeholder="Select category" />
915
+ </Select.Trigger>
916
+ </Select.Control>
917
+ </Select.Root>
918
+ </div>
919
+ </div>
920
+ </Drawer.Body>
921
+
922
+ <Drawer.Footer
923
+ className={css({
924
+ // Stick footer to bottom on mobile
925
+ position: isMobile ? 'sticky' : 'relative',
926
+ bottom: 0,
927
+ bg: 'bg.canvas',
928
+ borderTop: '1px solid',
929
+ borderColor: 'gray.4',
930
+ })}
931
+ >
932
+ <Button variant="outlined" onClick={() => setOpen(false)}>
933
+ Clear
934
+ </Button>
935
+ <Button variant="filled">Apply Filters</Button>
936
+ </Drawer.Footer>
937
+ </Drawer.Content>
938
+ </Drawer.Positioner>
939
+ </Drawer.Root>
940
+ ```
941
+
942
+ **Best practices:**
943
+
944
+ - Use `placement="bottom"` and `size="full"` for mobile screens
945
+ - Add safe area insets for devices with notches
946
+ - Make close buttons large and accessible on touch devices (min 44x44px)
947
+ - Stick important actions (footer) to viewport bottom
948
+ - Test gesture interactions (swipe to close) on mobile devices
949
+
950
+ ---
951
+
952
+ ### Nested Scrolling - Content Overflow
953
+
954
+ **Scenario:** Drawer content is taller than the viewport, requiring scrollable areas while keeping header and footer fixed.
955
+
956
+ **Solution:**
957
+
958
+ ```typescript
959
+ const [open, setOpen] = useState(false);
960
+
961
+ // Generate long content for demo
962
+ const longContent = Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`);
963
+
964
+ <Drawer.Root placement="end" size="md" open={open} onOpenChange={(details) => setOpen(details.open)}>
965
+ <Drawer.Trigger asChild>
966
+ <Button>View Long List</Button>
967
+ </Drawer.Trigger>
968
+
969
+ <Drawer.Backdrop />
970
+
971
+ <Drawer.Positioner>
972
+ <Drawer.Content
973
+ className={css({
974
+ display: 'flex',
975
+ flexDirection: 'column',
976
+ height: '100dvh', // Use dvh for mobile viewport height
977
+ maxHeight: '100dvh',
978
+ })}
979
+ >
980
+ {/* Fixed header */}
981
+ <Drawer.Header
982
+ className={css({
983
+ flexShrink: 0, // Prevent shrinking
984
+ borderBottom: '1px solid',
985
+ borderColor: 'gray.4',
986
+ position: 'sticky',
987
+ top: 0,
988
+ bg: 'bg.canvas',
989
+ zIndex: 1,
990
+ })}
991
+ >
992
+ <Drawer.Title>Scrollable Content</Drawer.Title>
993
+ <Drawer.Description>
994
+ This drawer has a long list that scrolls independently
995
+ </Drawer.Description>
996
+ <Drawer.CloseTrigger asChild>
997
+ <IconButton aria-label="Close" variant="text" size="sm">
998
+ <XIcon />
999
+ </IconButton>
1000
+ </Drawer.CloseTrigger>
1001
+ </Drawer.Header>
1002
+
1003
+ {/* Scrollable body */}
1004
+ <Drawer.Body
1005
+ className={css({
1006
+ flex: 1, // Take remaining space
1007
+ overflowY: 'auto', // Enable scrolling
1008
+ overflowX: 'hidden',
1009
+ WebkitOverflowScrolling: 'touch', // Smooth scrolling on iOS
1010
+ })}
1011
+ >
1012
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: '2', p: '4' })}>
1013
+ {longContent.map((item) => (
1014
+ <div
1015
+ key={item}
1016
+ className={css({
1017
+ p: '3',
1018
+ bg: 'gray.a2',
1019
+ borderRadius: 'md',
1020
+ })}
1021
+ >
1022
+ {item}
1023
+ </div>
1024
+ ))}
1025
+ </div>
1026
+ </Drawer.Body>
1027
+
1028
+ {/* Fixed footer */}
1029
+ <Drawer.Footer
1030
+ className={css({
1031
+ flexShrink: 0, // Prevent shrinking
1032
+ borderTop: '1px solid',
1033
+ borderColor: 'gray.4',
1034
+ position: 'sticky',
1035
+ bottom: 0,
1036
+ bg: 'bg.canvas',
1037
+ })}
1038
+ >
1039
+ <Button variant="outlined" onClick={() => setOpen(false)}>
1040
+ Cancel
1041
+ </Button>
1042
+ <Button variant="filled">Confirm</Button>
1043
+ </Drawer.Footer>
1044
+ </Drawer.Content>
1045
+ </Drawer.Positioner>
1046
+ </Drawer.Root>
1047
+ ```
1048
+
1049
+ **Best practices:**
1050
+
1051
+ - Use flexbox layout with `flex: 1` on body for proper scrolling
1052
+ - Make header and footer sticky with explicit backgrounds
1053
+ - Use `100dvh` instead of `100vh` for accurate mobile viewport height
1054
+ - Enable smooth scrolling on iOS with `-webkit-overflow-scrolling`
1055
+ - Test scrolling performance with large lists
1056
+ - Consider virtual scrolling for very long lists
1057
+
1058
+ ---
1059
+
1060
+ ### Backdrop Click - Preventing Close
1061
+
1062
+ **Scenario:** Prevent users from accidentally closing the drawer by clicking outside, requiring explicit close action.
1063
+
1064
+ **Solution:**
1065
+
1066
+ ```typescript
1067
+ const [open, setOpen] = useState(false);
1068
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
1069
+ const [showWarning, setShowWarning] = useState(false);
1070
+
1071
+ const handleInteractOutside = (event: Event) => {
1072
+ if (hasUnsavedChanges) {
1073
+ event.preventDefault(); // Prevent drawer from closing
1074
+ setShowWarning(true);
1075
+ }
1076
+ // If no unsaved changes, allow default behavior (drawer closes)
1077
+ };
1078
+
1079
+ const confirmClose = () => {
1080
+ setHasUnsavedChanges(false);
1081
+ setShowWarning(false);
1082
+ setOpen(false);
1083
+ };
1084
+
1085
+ <>
1086
+ <Drawer.Root
1087
+ open={open}
1088
+ onOpenChange={(details) => setOpen(details.open)}
1089
+ // closeOnInteractOutside={!hasUnsavedChanges} // Simple approach
1090
+ onInteractOutside={handleInteractOutside} // Advanced approach with warning
1091
+ closeOnEscapeKeyDown={!hasUnsavedChanges}
1092
+ >
1093
+ <Drawer.Trigger asChild>
1094
+ <Button>Edit Document</Button>
1095
+ </Drawer.Trigger>
1096
+
1097
+ <Drawer.Backdrop />
1098
+
1099
+ <Drawer.Positioner>
1100
+ <Drawer.Content>
1101
+ <Drawer.Header>
1102
+ <Drawer.Title>
1103
+ Edit Document
1104
+ {hasUnsavedChanges && (
1105
+ <span className={css({ ml: '2', fontSize: 'sm', color: 'warning.fg' })}>
1106
+ • Unsaved changes
1107
+ </span>
1108
+ )}
1109
+ </Drawer.Title>
1110
+ <Drawer.CloseTrigger asChild>
1111
+ <IconButton
1112
+ aria-label="Close"
1113
+ variant="text"
1114
+ size="sm"
1115
+ onClick={(e) => {
1116
+ if (hasUnsavedChanges) {
1117
+ e.preventDefault();
1118
+ setShowWarning(true);
1119
+ }
1120
+ }}
1121
+ >
1122
+ <XIcon />
1123
+ </IconButton>
1124
+ </Drawer.CloseTrigger>
1125
+ </Drawer.Header>
1126
+
1127
+ <Drawer.Body>
1128
+ <Textarea
1129
+ label="Content"
1130
+ rows={10}
1131
+ onChange={() => setHasUnsavedChanges(true)}
1132
+ placeholder="Start typing to trigger unsaved changes..."
1133
+ />
1134
+ </Drawer.Body>
1135
+
1136
+ <Drawer.Footer>
1137
+ <Button
1138
+ variant="outlined"
1139
+ onClick={() => {
1140
+ if (hasUnsavedChanges) {
1141
+ setShowWarning(true);
1142
+ } else {
1143
+ setOpen(false);
1144
+ }
1145
+ }}
1146
+ >
1147
+ Cancel
1148
+ </Button>
1149
+ <Button
1150
+ variant="filled"
1151
+ onClick={() => {
1152
+ // Save logic
1153
+ setHasUnsavedChanges(false);
1154
+ setOpen(false);
1155
+ }}
1156
+ >
1157
+ Save
1158
+ </Button>
1159
+ </Drawer.Footer>
1160
+ </Drawer.Content>
1161
+ </Drawer.Positioner>
1162
+ </Drawer.Root>
1163
+
1164
+ {/* Warning dialog */}
1165
+ {showWarning && (
1166
+ <Dialog
1167
+ open={showWarning}
1168
+ onOpenChange={({ open }) => setShowWarning(open)}
1169
+ title="Unsaved Changes"
1170
+ size="sm"
1171
+ >
1172
+ <div className={css({ p: 'lg' })}>
1173
+ <p className={css({ mb: 'lg' })}>
1174
+ You have unsaved changes. Are you sure you want to close without saving?
1175
+ </p>
1176
+ <div className={css({ display: 'flex', gap: 'sm', justifyContent: 'flex-end' })}>
1177
+ <Button variant="outlined" onClick={() => setShowWarning(false)}>
1178
+ Keep Editing
1179
+ </Button>
1180
+ <Button variant="filled" colorPalette="error" onClick={confirmClose}>
1181
+ Discard Changes
1182
+ </Button>
1183
+ </div>
1184
+ </div>
1185
+ </Dialog>
1186
+ )}
1187
+ </>
1188
+ ```
1189
+
1190
+ **Best practices:**
1191
+
1192
+ - Use `closeOnInteractOutside={false}` for critical forms
1193
+ - Show clear visual indicators for unsaved changes
1194
+ - Provide explicit save/discard options
1195
+ - Use confirmation dialogs for destructive actions
1196
+ - Allow Escape key close only when safe
1197
+ - Communicate blocked close actions with visual feedback
1198
+
1199
+ ---
1200
+
1201
+ ### Animation Interruption - Opening/Closing During Animation
1202
+
1203
+ **Scenario:** User rapidly toggles drawer open/close, causing animation interruptions and potential state issues.
1204
+
1205
+ **Solution:**
1206
+
1207
+ ```typescript
1208
+ const [open, setOpen] = useState(false);
1209
+ const [isAnimating, setIsAnimating] = useState(false);
1210
+ const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
1211
+
1212
+ const handleOpenChange = (details: { open: boolean }) => {
1213
+ // Clear any pending animation timeout
1214
+ if (animationTimeoutRef.current) {
1215
+ clearTimeout(animationTimeoutRef.current);
1216
+ }
1217
+
1218
+ // Set animating state
1219
+ setIsAnimating(true);
1220
+
1221
+ // Update open state
1222
+ setOpen(details.open);
1223
+
1224
+ // Animation durations from design system
1225
+ // Opening: slowest (500ms), Closing: normal (250ms)
1226
+ const animationDuration = details.open ? 500 : 250;
1227
+
1228
+ // Clear animating state after animation completes
1229
+ animationTimeoutRef.current = setTimeout(() => {
1230
+ setIsAnimating(false);
1231
+ }, animationDuration);
1232
+ };
1233
+
1234
+ // Cleanup on unmount
1235
+ useEffect(() => {
1236
+ return () => {
1237
+ if (animationTimeoutRef.current) {
1238
+ clearTimeout(animationTimeoutRef.current);
1239
+ }
1240
+ };
1241
+ }, []);
1242
+
1243
+ <div>
1244
+ <div className={css({ mb: '4' })}>
1245
+ <Button onClick={() => handleOpenChange({ open: true })} disabled={isAnimating}>
1246
+ Open Drawer
1247
+ </Button>
1248
+ <span className={css({ ml: '2', fontSize: 'sm', color: 'fg.muted' })}>
1249
+ {isAnimating ? 'Animating...' : 'Ready'}
1250
+ </span>
1251
+ </div>
1252
+
1253
+ <Drawer.Root
1254
+ open={open}
1255
+ onOpenChange={handleOpenChange}
1256
+ // Disable interactions during animation
1257
+ modal={!isAnimating}
1258
+ >
1259
+ <Drawer.Backdrop
1260
+ className={css({
1261
+ // Ensure backdrop respects animation state
1262
+ pointerEvents: isAnimating ? 'none' : 'auto',
1263
+ })}
1264
+ />
1265
+
1266
+ <Drawer.Positioner>
1267
+ <Drawer.Content
1268
+ className={css({
1269
+ // Prevent content interaction during animation
1270
+ pointerEvents: isAnimating ? 'none' : 'auto',
1271
+ })}
1272
+ >
1273
+ <Drawer.Header>
1274
+ <Drawer.Title>Animated Drawer</Drawer.Title>
1275
+ <Drawer.CloseTrigger asChild>
1276
+ <IconButton
1277
+ aria-label="Close"
1278
+ variant="text"
1279
+ size="sm"
1280
+ disabled={isAnimating}
1281
+ >
1282
+ <XIcon />
1283
+ </IconButton>
1284
+ </Drawer.CloseTrigger>
1285
+ </Drawer.Header>
1286
+
1287
+ <Drawer.Body>
1288
+ <div className={css({ p: '4' })}>
1289
+ <p>Try rapidly toggling the drawer to see smooth animation handling.</p>
1290
+ <Button
1291
+ className={css({ mt: '4' })}
1292
+ onClick={() => handleOpenChange({ open: false })}
1293
+ disabled={isAnimating}
1294
+ >
1295
+ Close from Inside
1296
+ </Button>
1297
+ </div>
1298
+ </Drawer.Body>
1299
+ </Drawer.Content>
1300
+ </Drawer.Positioner>
1301
+ </Drawer.Root>
1302
+ </div>
1303
+ ```
1304
+
1305
+ **Best practices:**
1306
+
1307
+ - Track animation state to prevent interaction during transitions
1308
+ - Clear pending timeouts when animations are interrupted
1309
+ - Disable trigger buttons during animation to prevent rapid toggling
1310
+ - Use design system animation durations for consistency
1311
+ - Set `pointer-events: none` on animating elements
1312
+ - Cleanup animation timers on component unmount
1313
+ - Consider using animation events (`onAnimationEnd`) for more precise timing
1314
+
1315
+ ---
1316
+
1317
+ ## DO NOT
1318
+
1319
+ ```typescript
1320
+ // ❌ Don't omit Backdrop (unless intentional)
1321
+ <Drawer.Root>
1322
+ <Drawer.Positioner>
1323
+ <Drawer.Content>...</Drawer.Content>
1324
+ </Drawer.Positioner>
1325
+ </Drawer.Root>
1326
+
1327
+ // ❌ Don't forget Positioner wrapper
1328
+ <Drawer.Root>
1329
+ <Drawer.Backdrop />
1330
+ <Drawer.Content>...</Drawer.Content> // Missing Positioner
1331
+ </Drawer.Root>
1332
+
1333
+ // ❌ Don't omit Title (required for accessibility)
1334
+ <Drawer.Content>
1335
+ <Drawer.Body>
1336
+ Content without title
1337
+ </Drawer.Body>
1338
+ </Drawer.Content>
1339
+
1340
+ // ❌ Don't use for critical alerts or confirmations (use Dialog instead)
1341
+ <Drawer.Root>
1342
+ <Drawer.Content>
1343
+ <Drawer.Title>Delete Account?</Drawer.Title>
1344
+ <Drawer.Body>This action cannot be undone</Drawer.Body>
1345
+ </Drawer.Content>
1346
+ </Drawer.Root>
1347
+
1348
+ // ❌ Don't nest drawers
1349
+ <Drawer.Root>
1350
+ <Drawer.Content>
1351
+ <Drawer.Root> // Don't nest
1352
+ <Drawer.Content>...</Drawer.Content>
1353
+ </Drawer.Root>
1354
+ </Drawer.Content>
1355
+ </Drawer.Root>
1356
+
1357
+ // ❌ Don't use oversized drawers for simple content
1358
+ <Drawer.Root size="xl">
1359
+ <Drawer.Content>
1360
+ <Drawer.Body>
1361
+ <p>Just a simple message</p> // Use smaller size
1362
+ </Drawer.Body>
1363
+ </Drawer.Content>
1364
+ </Drawer.Root>
1365
+
1366
+ // ❌ Don't put primary navigation in end-placed drawer
1367
+ <Drawer.Root placement="end"> // Use placement="start" for navigation
1368
+ <Drawer.Content>
1369
+ <nav>Main navigation menu</nav>
1370
+ </Drawer.Content>
1371
+ </Drawer.Root>
1372
+
1373
+ // ✅ Correct usage
1374
+ <Drawer.Root placement="start">
1375
+ <Drawer.Trigger asChild>
1376
+ <Button>Menu</Button>
1377
+ </Drawer.Trigger>
1378
+ <Drawer.Backdrop />
1379
+ <Drawer.Positioner>
1380
+ <Drawer.Content>
1381
+ <Drawer.Header>
1382
+ <Drawer.Title>Navigation</Drawer.Title>
1383
+ <Drawer.CloseTrigger asChild>
1384
+ <IconButton aria-label="Close menu">
1385
+ <XIcon />
1386
+ </IconButton>
1387
+ </Drawer.CloseTrigger>
1388
+ </Drawer.Header>
1389
+ <Drawer.Body>
1390
+ <nav>Navigation items</nav>
1391
+ </Drawer.Body>
1392
+ </Drawer.Content>
1393
+ </Drawer.Positioner>
1394
+ </Drawer.Root>
1395
+ ```
1396
+
1397
+ ## Accessibility
1398
+
1399
+ The Drawer component follows WCAG 2.1 Level AA standards:
1400
+
1401
+ - **Focus Management**: Focus trapped within drawer when open
1402
+ - **Keyboard Navigation**:
1403
+ - `Escape` key closes drawer
1404
+ - `Tab` cycles through focusable elements
1405
+ - Focus returns to trigger on close
1406
+ - **Screen Reader Support**:
1407
+ - Announced as dialog/modal
1408
+ - Title required for proper announcement
1409
+ - Description recommended for context
1410
+ - **ARIA Attributes**:
1411
+ - `role="dialog"` on Content
1412
+ - `aria-modal="true"` when modal
1413
+ - `aria-labelledby` references Title
1414
+ - `aria-describedby` references Description
1415
+ - **Body Scroll Lock**: Prevents background scrolling when open
1416
+
1417
+ ### Accessibility Best Practices
1418
+
1419
+ ```typescript
1420
+ // ✅ Always provide Title
1421
+ <Drawer.Content>
1422
+ <Drawer.Header>
1423
+ <Drawer.Title>Settings</Drawer.Title>
1424
+ </Drawer.Header>
1425
+ </Drawer.Content>
1426
+
1427
+ // ✅ Add Description for complex drawers
1428
+ <Drawer.Header>
1429
+ <Drawer.Title>Export Data</Drawer.Title>
1430
+ <Drawer.Description>
1431
+ Choose format and options for exporting your data
1432
+ </Drawer.Description>
1433
+ </Drawer.Header>
1434
+
1435
+ // ✅ Label close buttons
1436
+ <Drawer.CloseTrigger asChild>
1437
+ <IconButton aria-label="Close drawer">
1438
+ <XIcon />
1439
+ </IconButton>
1440
+ </Drawer.CloseTrigger>
1441
+
1442
+ // ✅ Use semantic HTML in content
1443
+ <Drawer.Body>
1444
+ <nav aria-label="Main navigation">
1445
+ <ul>
1446
+ <li><a href="/">Home</a></li>
1447
+ <li><a href="/about">About</a></li>
1448
+ </ul>
1449
+ </nav>
1450
+ </Drawer.Body>
1451
+
1452
+ // ✅ Announce dynamic changes
1453
+ <Drawer.Body>
1454
+ <form aria-live="polite">
1455
+ {/* Form with validation messages */}
1456
+ </form>
1457
+ </Drawer.Body>
1458
+ ```
1459
+
1460
+ ## Usage Guidelines
1461
+
1462
+ ### When to Use Drawer
1463
+
1464
+ | Use Case | Why Drawer |
1465
+ | --------------- | --------------------------------------------- |
1466
+ | Navigation menu | Slides from side, doesn't block entire screen |
1467
+ | Filter panel | Related to page content, easy to dismiss |
1468
+ | Shopping cart | Contextual preview without leaving page |
1469
+ | Settings panel | Secondary actions, can stay on current page |
1470
+ | Detail view | Additional info without full page navigation |
1471
+ | Form/wizard | Multi-step process without modal interruption |
1472
+
1473
+ ### When NOT to Use Drawer
1474
+
1475
+ | Use Case | Use Instead | Why |
1476
+ | ---------------------- | --------------- | -------------------------------------------- |
1477
+ | Critical confirmations | Dialog | Center focus, harder to dismiss accidentally |
1478
+ | Short messages | Toast/Alert | Drawer is too heavy for simple notifications |
1479
+ | Quick tips | Tooltip/Popover | Drawer is overkill for small hints |
1480
+ | Full page forms | Separate route | Better for complex, primary content |
1481
+ | Nested panels | Tabs/Accordion | Avoid drawer inception |
1482
+
1483
+ ## Placement Guidelines
1484
+
1485
+ | Placement | Best For | Direction Support |
1486
+ | --------- | ------------------------- | ------------------------- |
1487
+ | `start` | Primary navigation, menus | LTR: left, RTL: right |
1488
+ | `end` | Carts, details, settings | LTR: right, RTL: left |
1489
+ | `top` | Notifications, banners | Top edge (all locales) |
1490
+ | `bottom` | Mobile sheets, actions | Bottom edge (all locales) |
1491
+
1492
+ ## Size Guidelines
1493
+
1494
+ | Size | Width/Height | Best For |
1495
+ | ------ | ------------ | --------------------------------- |
1496
+ | `xs` | 320px | Minimal menus, quick filters |
1497
+ | `sm` | 384px | Standard navigation, simple forms |
1498
+ | `md` | 448px | Detailed content, shopping cart |
1499
+ | `lg` | 512px | Rich panels, multi-section forms |
1500
+ | `xl` | 576px | Dashboard panels, complex content |
1501
+ | `full` | 100% | Mobile takeover, full editing |
1502
+
1503
+ ## State Behaviors
1504
+
1505
+ | State | Visual Change | Behavior |
1506
+ | ----------- | ---------------------- | ----------------------------------------- |
1507
+ | **Opening** | Slides in + fades in | Duration: slowest (emphasized-in easing) |
1508
+ | **Open** | Fully visible | Focus trapped, body scroll locked |
1509
+ | **Closing** | Slides out + fades out | Duration: normal (emphasized-out easing) |
1510
+ | **Closed** | Removed from DOM | Focus returns to trigger, scroll restored |
1511
+
1512
+ ## Testing
1513
+
1514
+ When testing Drawer components:
1515
+
1516
+ ```typescript
1517
+ import { render, screen, waitFor } from '@testing-library/react';
1518
+ import userEvent from '@testing-library/user-event';
1519
+ import { Drawer, Button } from '@discourser/design-system';
1520
+
1521
+ test('drawer opens and closes', async () => {
1522
+ const user = userEvent.setup();
1523
+
1524
+ render(
1525
+ <Drawer.Root>
1526
+ <Drawer.Trigger asChild>
1527
+ <Button>Open</Button>
1528
+ </Drawer.Trigger>
1529
+ <Drawer.Backdrop />
1530
+ <Drawer.Positioner>
1531
+ <Drawer.Content>
1532
+ <Drawer.Title>Test Drawer</Drawer.Title>
1533
+ <Drawer.Body>Content</Drawer.Body>
1534
+ <Drawer.CloseTrigger asChild>
1535
+ <Button>Close</Button>
1536
+ </Drawer.CloseTrigger>
1537
+ </Drawer.Content>
1538
+ </Drawer.Positioner>
1539
+ </Drawer.Root>
1540
+ );
1541
+
1542
+ // Initially closed
1543
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
1544
+
1545
+ // Open drawer
1546
+ await user.click(screen.getByText('Open'));
1547
+
1548
+ await waitFor(() => {
1549
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
1550
+ expect(screen.getByText('Test Drawer')).toBeInTheDocument();
1551
+ });
1552
+
1553
+ // Close drawer
1554
+ await user.click(screen.getByText('Close'));
1555
+
1556
+ await waitFor(() => {
1557
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
1558
+ });
1559
+ });
1560
+
1561
+ test('drawer closes on backdrop click', async () => {
1562
+ const user = userEvent.setup();
1563
+ const onOpenChange = vi.fn();
1564
+
1565
+ render(
1566
+ <Drawer.Root onOpenChange={onOpenChange}>
1567
+ <Drawer.Trigger asChild>
1568
+ <Button>Open</Button>
1569
+ </Drawer.Trigger>
1570
+ <Drawer.Backdrop />
1571
+ <Drawer.Positioner>
1572
+ <Drawer.Content>
1573
+ <Drawer.Title>Test</Drawer.Title>
1574
+ </Drawer.Content>
1575
+ </Drawer.Positioner>
1576
+ </Drawer.Root>
1577
+ );
1578
+
1579
+ await user.click(screen.getByText('Open'));
1580
+
1581
+ const backdrop = screen.getByRole('dialog').parentElement;
1582
+ await user.click(backdrop!);
1583
+
1584
+ expect(onOpenChange).toHaveBeenCalledWith({ open: false });
1585
+ });
1586
+
1587
+ test('drawer closes on escape key', async () => {
1588
+ const user = userEvent.setup();
1589
+
1590
+ render(
1591
+ <Drawer.Root defaultOpen>
1592
+ <Drawer.Positioner>
1593
+ <Drawer.Content>
1594
+ <Drawer.Title>Test</Drawer.Title>
1595
+ </Drawer.Content>
1596
+ </Drawer.Positioner>
1597
+ </Drawer.Root>
1598
+ );
1599
+
1600
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
1601
+
1602
+ await user.keyboard('{Escape}');
1603
+
1604
+ await waitFor(() => {
1605
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
1606
+ });
1607
+ });
1608
+ ```
1609
+
1610
+ ## Related Components
1611
+
1612
+ - **Dialog**: Use for centered modals and critical confirmations
1613
+ - **Popover**: Use for contextual menus and tooltips
1614
+ - **Sheet**: Mobile-specific bottom sheet (Drawer with `placement="bottom"`)
1615
+ - **Menu**: Use for dropdown menus and context menus
1616
+ - **Tooltip**: Use for simple hints and help text