@discourser/design-system 0.4.0 → 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 +67 -123
  4. package/guidelines/components/accordion.md +93 -0
  5. package/guidelines/components/avatar.md +70 -0
  6. package/guidelines/components/badge.md +61 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +88 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +655 -0
  12. package/guidelines/components/heading.md +71 -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 +71 -0
  18. package/guidelines/components/progress.md +63 -0
  19. package/guidelines/components/radio-group.md +95 -0
  20. package/guidelines/components/select.md +507 -0
  21. package/guidelines/components/skeleton.md +76 -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 +654 -0
  26. package/guidelines/components/textarea.md +70 -0
  27. package/guidelines/components/toast.md +77 -0
  28. package/guidelines/components/tooltip.md +80 -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 +9 -5
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -2,6 +2,97 @@
2
2
 
3
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
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
+
5
96
  ## Import
6
97
 
7
98
  ```typescript
@@ -659,6 +750,570 @@ function ResponsiveDrawer() {
659
750
  }
660
751
  ```
661
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
+
662
1317
  ## DO NOT
663
1318
 
664
1319
  ```typescript