@abhivarde/svelte-drawer 1.0.7 → 2.0.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.
package/README.md CHANGED
@@ -7,23 +7,25 @@ A drawer component for Svelte 5, inspired by [Vaul](https://github.com/emilkowal
7
7
 
8
8
  ## Features
9
9
 
10
- - Smooth animations with **gesture-driven dragging** (mouse & touch)
11
- - Mobile-optimized drag handling with **scroll prevention**
12
- - Support for multiple directions (**bottom, top, left, right**)
13
- - Prebuilt variants (**default, sheet, dialog, minimal, sidebar**)
14
- - **Drag handle component** with auto-adaptive orientation
15
- - **Snap points** for iOS-like multi-height drawers
16
- - ✅ **Portal rendering** to escape z-index conflicts
17
- - **Optional header & footer** components for quick setup
18
- - **Auto height** for dynamic content (AI streaming, forms, dynamic lists)
19
- - **Configurable dismiss threshold** via `closeThreshold` prop
20
- - Nested drawer support
21
- - Scrollable content areas
22
- - Keyboard shortcuts (**Escape to close**, Tab navigation)
23
- - Focus management (**auto-focus, focus trap, focus restoration**)
24
- - Fully accessible with keyboard navigation
25
- - Full **TypeScript** support
26
- - Customizable styling with **Tailwind CSS**
10
+ - Smooth animations with gesture-driven dragging (mouse and touch)
11
+ - Support for multiple directions (bottom, top, left, right)
12
+ - Customizable animation duration and easing per drawer
13
+ - Snap points for iOS-like multi-height drawers
14
+ - Auto height for dynamic content (AI streaming, forms, lists)
15
+ - Backdrop blur on the overlay
16
+ - Portal rendering to escape z-index conflicts
17
+ - Prebuilt variants (default, sheet, dialog, minimal, sidebar)
18
+ - Drag handle with auto-adaptive orientation
19
+ - Optional header and footer components
20
+ - Nested drawer support
21
+ - Scrollable content areas
22
+ - Persistent state across page reloads
23
+ - Configurable dismiss threshold
24
+ - Keyboard shortcuts (Escape to close, Tab navigation)
25
+ - Focus management (auto-focus, focus trap, focus restoration)
26
+ - Fully accessible with keyboard navigation
27
+ - Full TypeScript support
28
+ - Works with Tailwind CSS
27
29
 
28
30
  ## Installation
29
31
 
@@ -33,435 +35,262 @@ npm install @abhivarde/svelte-drawer
33
35
 
34
36
  ## Usage
35
37
 
36
- ### Basic Example
38
+ ### Basic
37
39
 
38
40
  ```svelte
39
41
  <script>
40
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
42
+ import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
41
43
 
42
- let open = $state(false);
44
+ let open = $state(false);
43
45
  </script>
44
46
 
45
- <button onclick={() => open = true}>
46
- Open Drawer
47
- </button>
47
+ <button onclick={() => open = true}>Open Drawer</button>
48
48
 
49
49
  <Drawer bind:open>
50
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
51
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
52
- <DrawerHandle class="mb-8" />
53
- <h2>Drawer Content</h2>
54
- <p>This is a drawer component.</p>
55
- <button onclick={() => open = false}>Close</button>
56
- </DrawerContent>
50
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
51
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
52
+ <DrawerHandle class="mb-8" />
53
+ <h2>Drawer Content</h2>
54
+ <p>This is a drawer component.</p>
55
+ <button onclick={() => open = false}>Close</button>
56
+ </DrawerContent>
57
57
  </Drawer>
58
58
  ```
59
59
 
60
- ### Backdrop Blur
61
-
62
- Add a premium blur effect to the overlay background:
60
+ ### Directions
63
61
 
64
62
  ```svelte
65
- <script>
66
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
67
-
68
- let open = $state(false);
69
- </script>
70
-
71
- <Drawer bind:open>
72
- <!-- Default medium blur -->
73
- <DrawerOverlay blur class="fixed inset-0 bg-black/40" />
74
-
75
- <!-- Or specify blur intensity -->
76
- <!-- <DrawerOverlay blur="sm" class="fixed inset-0 bg-black/40" /> -->
77
- <!-- <DrawerOverlay blur="lg" class="fixed inset-0 bg-black/40" /> -->
78
- <!-- <DrawerOverlay blur="xl" class="fixed inset-0 bg-black/40" /> -->
79
-
80
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
81
- <DrawerHandle class="mb-8" />
82
- <h2>Blurred Backdrop</h2>
83
- <p>Notice the premium blur effect behind this drawer.</p>
84
- </DrawerContent>
85
- </Drawer>
63
+ <Drawer bind:open direction="bottom">...</Drawer>
64
+ <Drawer bind:open direction="top">...</Drawer>
65
+ <Drawer bind:open direction="left">...</Drawer>
66
+ <Drawer bind:open direction="right">...</Drawer>
86
67
  ```
87
68
 
88
- **Available blur intensities:**
69
+ ### Custom Animation
89
70
 
90
- - `blur={true}` or `blur="md"` - Medium blur (default)
91
- - `blur="sm"` - Small blur
92
- - `blur="lg"` - Large blur
93
- - `blur="xl"` - Extra large blur
94
- - `blur="2xl"` - 2x extra large blur
95
- - `blur="3xl"` - 3x extra large blur
96
-
97
- ### Side Drawer
71
+ Control the speed and easing of any drawer independently.
98
72
 
99
73
  ```svelte
100
74
  <script>
101
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
102
-
103
- let open = $state(false);
75
+ import { cubicOut, bounceOut, linear } from 'svelte/easing';
104
76
  </script>
105
77
 
106
- <Drawer bind:open direction="right">
107
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
108
- <DrawerContent class="fixed right-0 top-0 bottom-0 w-80 bg-white p-4">
109
- <DrawerHandle class="mb-4" />
110
- <h2>Side Drawer</h2>
111
- <button onclick={() => open = false}>Close</button>
112
- </DrawerContent>
78
+ <!-- slower, softer -->
79
+ <Drawer bind:open animationDuration={400} animationEasing={cubicOut}>
80
+ ...
113
81
  </Drawer>
114
- ```
115
82
 
116
- ### Controlled Drawer
83
+ <!-- fast and snappy -->
84
+ <Drawer bind:open animationDuration={150} animationEasing={linear}>
85
+ ...
86
+ </Drawer>
117
87
 
118
- ```svelte
119
- <script>
120
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
88
+ <!-- playful bounce -->
89
+ <Drawer bind:open animationDuration={500} animationEasing={bounceOut}>
90
+ ...
91
+ </Drawer>
92
+ ```
121
93
 
122
- let open = $state(false);
94
+ Any easing from `svelte/easing` works, or pass your own `(t: number) => number` function. Default is `220ms` with `expoOut`.
123
95
 
124
- function handleOpenChange(isOpen) {
125
- console.log('Drawer is now:', isOpen ? 'open' : 'closed');
126
- open = isOpen;
127
- }
128
- </script>
96
+ ### Side Drawer
129
97
 
130
- <Drawer bind:open onOpenChange={handleOpenChange}>
131
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
132
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
133
- <DrawerHandle class="mb-8" />
134
- <h2>Controlled Drawer</h2>
135
- </DrawerContent>
98
+ ```svelte
99
+ <Drawer bind:open direction="right">
100
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
101
+ <DrawerContent class="fixed right-0 top-0 bottom-0 w-80 bg-white p-4">
102
+ <DrawerHandle class="mb-4" />
103
+ <h2>Side Drawer</h2>
104
+ </DrawerContent>
136
105
  </Drawer>
137
106
  ```
138
107
 
139
- ### Disable Keyboard Features
108
+ ### Backdrop Blur
140
109
 
141
110
  ```svelte
142
- <script>
143
- import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
144
-
145
- let open = $state(false);
146
- </script>
147
-
148
- <!-- Disable Escape key -->
149
- <Drawer bind:open closeOnEscape={false}>
150
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
151
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
152
- <h2>Cannot close with Escape</h2>
153
- </DrawerContent>
154
- </Drawer>
155
-
156
- <!-- Disable focus trap -->
157
111
  <Drawer bind:open>
158
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
159
- <DrawerContent trapFocus={false} class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
160
- <h2>Tab navigation not restricted</h2>
161
- </DrawerContent>
112
+ <DrawerOverlay blur="lg" class="fixed inset-0 bg-black/30" />
113
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
114
+ <DrawerHandle class="mb-8" />
115
+ <h2>Blurred Backdrop</h2>
116
+ </DrawerContent>
162
117
  </Drawer>
163
118
  ```
164
119
 
165
- ### Close Threshold
120
+ Available intensities: `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`. Default is `md`.
166
121
 
167
- Control how far the user needs to drag before the drawer dismisses.
122
+ ### Snap Points
168
123
 
169
124
  ```svelte
170
125
  <script>
171
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
172
-
173
126
  let open = $state(false);
127
+ let activeSnapPoint = $state(undefined);
174
128
  </script>
175
129
 
176
- <!-- easier to dismiss (short drag) -->
177
- <Drawer bind:open closeThreshold={0.15}>
178
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
179
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
180
- <DrawerHandle class="mb-8" />
181
- <p>Short drag closes this drawer.</p>
182
- </DrawerContent>
183
- </Drawer>
184
-
185
- <!-- harder to dismiss (long drag required) -->
186
- <Drawer bind:open closeThreshold={0.5}>
130
+ <Drawer
131
+ bind:open
132
+ snapPoints={[0.25, 0.5, 0.9]}
133
+ bind:activeSnapPoint
134
+ onSnapPointChange={(point) => console.log('Snapped to:', point)}
135
+ >
187
136
  <DrawerOverlay class="fixed inset-0 bg-black/40" />
188
137
  <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
189
138
  <DrawerHandle class="mb-8" />
190
- <p>Requires a longer drag to close.</p>
139
+ <p>Current: {activeSnapPoint ? `${(activeSnapPoint * 100).toFixed(0)}%` : 'loading'}</p>
140
+ <button onclick={() => activeSnapPoint = 0.5}>Jump to 50%</button>
191
141
  </DrawerContent>
192
142
  </Drawer>
193
143
  ```
194
144
 
195
- Values range from `0` to `1`. Default is `0.3` (30% of the viewport).
145
+ Snap point values range from 0 to 1. The drawer snaps to the nearest point on release. Dragging past the lowest snap point dismisses it.
196
146
 
197
- ### Using Variants
147
+ ### Auto Height
148
+
149
+ Use `autoHeight` on `DrawerContent` when content changes height at runtime, such as AI streaming responses, multi-step forms, or dynamic lists.
198
150
 
199
151
  ```svelte
200
152
  <script>
201
- import { Drawer, DrawerOverlay, DrawerVariants, DrawerHandle } from '@abhivarde/svelte-drawer';
153
+ let open = $state(false);
154
+ let streaming = $state(false);
155
+ let text = $state('');
202
156
 
203
- let open = $state(false);
157
+ async function simulate() {
158
+ open = true;
159
+ streaming = true;
160
+ text = '';
161
+ const lines = [
162
+ 'Sure! Here is what autoHeight does.',
163
+ '\n\nIt watches your content as it changes.',
164
+ '\n\nWhen content grows, the drawer follows automatically.',
165
+ '\n\nNo magic numbers. No hardcoded heights. Just works.',
166
+ ];
167
+ for (const line of lines) {
168
+ await new Promise(r => setTimeout(r, 500));
169
+ text += line;
170
+ }
171
+ streaming = false;
172
+ }
204
173
  </script>
205
174
 
206
- <!-- Sheet variant (iOS-style bottom sheet) -->
207
- <Drawer bind:open>
208
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
209
- <DrawerVariants variant="sheet">
210
- <div class="p-6">
211
- <DrawerHandle class="mb-6" />
212
- <h2>iOS-style Sheet</h2>
213
- <p>Clean and modern bottom sheet design</p>
214
- </div>
215
- </DrawerVariants>
216
- </Drawer>
175
+ <button onclick={simulate}>Ask AI</button>
217
176
 
218
- <!-- Dialog variant (center modal) -->
219
177
  <Drawer bind:open>
220
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
221
- <DrawerVariants variant="dialog">
222
- <div class="p-6">
223
- <h2>Dialog Style</h2>
224
- <p>Center-positioned modal dialog</p>
225
- </div>
226
- </DrawerVariants>
227
- </Drawer>
228
-
229
- <!-- Sidebar variant (navigation drawer) -->
230
- <Drawer bind:open direction="right">
231
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
232
- <DrawerVariants variant="sidebar">
233
- <div class="p-6">
234
- <DrawerHandle class="mb-4" />
235
- <h2>Sidebar Navigation</h2>
236
- <p>Side navigation drawer</p>
237
- </div>
238
- </DrawerVariants>
178
+ <DrawerOverlay />
179
+ <DrawerContent
180
+ autoHeight
181
+ class="bg-gray-100 flex flex-col rounded-t-[10px] fixed bottom-0 left-0 right-0 outline-none"
182
+ >
183
+ <div class="p-4 bg-white rounded-t-[10px]">
184
+ <DrawerHandle class="mb-8" />
185
+ <p class="font-medium mb-2 text-gray-900">AI Response</p>
186
+ {#if text}
187
+ <p class="text-sm text-gray-600 leading-relaxed">
188
+ {text}{streaming ? '▌' : ''}
189
+ </p>
190
+ {/if}
191
+ </div>
192
+ </DrawerContent>
239
193
  </Drawer>
240
194
  ```
241
195
 
242
- ### Drag Handle Customization
243
-
244
- ```svelte
245
- <script>
246
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
247
-
248
- let open = $state(false);
249
- </script>
250
-
251
- <Drawer bind:open>
252
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
253
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
254
- <!-- Default gray handle -->
255
- <DrawerHandle class="mb-8" />
196
+ ### Close Threshold
256
197
 
257
- <!-- Custom colored handle -->
258
- <!-- <DrawerHandle class="bg-blue-500 mb-8" /> -->
198
+ Control how far the user must drag before the drawer dismisses.
259
199
 
260
- <!-- Larger handle -->
261
- <!-- <DrawerHandle class="w-16 h-2 mb-8" /> -->
200
+ ```svelte
201
+ <!-- easier to dismiss -->
202
+ <Drawer bind:open closeThreshold={0.15}>...</Drawer>
262
203
 
263
- <h2>Drawer with Custom Handle</h2>
264
- <p>The handle automatically adapts to drawer direction.</p>
265
- </DrawerContent>
266
- </Drawer>
204
+ <!-- harder to dismiss -->
205
+ <Drawer bind:open closeThreshold={0.5}>...</Drawer>
267
206
  ```
268
207
 
269
- ### Snap Points
208
+ Values range from `0` to `1`. Default is `0.3`.
270
209
 
271
- Snap points allow the drawer to rest at predefined heights, creating an iOS-like sheet experience.
210
+ ### Variants
272
211
 
273
212
  ```svelte
274
- <script>
275
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
276
-
277
- let open = $state(false);
278
- let activeSnapPoint = $state(undefined);
279
- </script>
213
+ <!-- iOS-style sheet -->
214
+ <Drawer bind:open>
215
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
216
+ <DrawerVariants variant="sheet">
217
+ <DrawerHandle class="mb-6" />
218
+ <h2>Sheet</h2>
219
+ </DrawerVariants>
220
+ </Drawer>
280
221
 
281
- <Drawer
282
- bind:open
283
- snapPoints={[0.25, 0.5, 0.9]}
284
- bind:activeSnapPoint
285
- onSnapPointChange={(point) => console.log('Snapped to:', point)}
286
- >
287
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
288
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
289
- <DrawerHandle class="mb-8" />
290
- <h2>Drawer with Snap Points</h2>
291
- <p>Drag to see snapping behavior at 25%, 50%, and 90%</p>
292
-
293
- <!-- Programmatically change snap point -->
294
- <button onclick={() => activeSnapPoint = 0.5}>Jump to 50%</button>
295
- </DrawerContent>
222
+ <!-- sidebar -->
223
+ <Drawer bind:open direction="right">
224
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
225
+ <DrawerVariants variant="sidebar">
226
+ <h2>Sidebar</h2>
227
+ </DrawerVariants>
296
228
  </Drawer>
297
229
  ```
298
230
 
299
- **How it works:**
300
-
301
- - Snap point values range from 0 to 1 (e.g., `0.5` = 50% of screen height)
302
- - The drawer automatically snaps to the nearest point when released
303
- - Dragging beyond the lowest snap point dismisses the drawer
304
- - Use `bind:activeSnapPoint` to programmatically control the current position
305
- - Use `onSnapPointChange` callback to react to snap changes
231
+ Available: `default`, `sheet`, `dialog`, `minimal`, `sidebar`.
306
232
 
307
- ### Portal Support
233
+ ### Portal
308
234
 
309
- Render the drawer in a portal to avoid z-index conflicts in complex layouts.
235
+ Renders the drawer at the end of `<body>` to avoid z-index and overflow conflicts.
310
236
 
311
237
  ```svelte
312
- <script>
313
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
314
-
315
- let open = $state(false);
316
- </script>
317
-
318
- <!-- Enable portal (renders at end of body) -->
319
238
  <Drawer bind:open portal={true}>
320
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
321
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
322
- <DrawerHandle class="mb-8" />
323
- <h2>Portal Drawer</h2>
324
- <p>This drawer is rendered in a portal, preventing z-index issues.</p>
325
- </DrawerContent>
326
- </Drawer>
327
-
328
- <!-- Custom portal container -->
329
- <Drawer bind:open portal={true} portalContainer="#custom-portal">
330
- <DrawerOverlay />
331
- <DrawerContent>
332
- <h2>Custom Portal</h2>
333
- </DrawerContent>
239
+ <DrawerOverlay />
240
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
241
+ <DrawerHandle class="mb-8" />
242
+ <h2>Portal Drawer</h2>
243
+ </DrawerContent>
334
244
  </Drawer>
335
-
336
- <div id="custom-portal"></div>
337
245
  ```
338
246
 
339
- **When to use portals:**
340
-
341
- - Complex layouts with nested z-index contexts
342
- - Third-party component libraries with fixed positioning
343
- - Modals inside scrollable containers
344
- - Preventing overflow: hidden conflicts
345
-
346
- ### Header & Footer Components
347
-
348
- Optional pre-styled header and footer components for quick setup.
349
-
350
- #### DrawerHeader
247
+ Custom container:
351
248
 
352
249
  ```svelte
353
- <script>
354
- import { Drawer, DrawerOverlay, DrawerContent, DrawerHeader, DrawerHandle } from '@abhivarde/svelte-drawer';
355
-
356
- let open = $state(false);
357
- </script>
358
-
359
- <!-- With title and description -->
360
- <Drawer bind:open>
361
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
362
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg">
363
- <DrawerHeader
364
- title="Drawer Title"
365
- description="Optional description text"
366
- showCloseButton={true}
367
- />
368
- <div class="p-4">
369
- <p>Drawer content here</p>
370
- </div>
371
- </DrawerContent>
250
+ <Drawer bind:open portal={true} portalContainer="#my-portal">
251
+ ...
372
252
  </Drawer>
373
253
 
374
- <!-- Custom header content -->
375
- <Drawer bind:open>
376
- <DrawerOverlay />
377
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg">
378
- <DrawerHeader>
379
- <div class="flex items-center gap-3">
380
- <img src="/icon.png" alt="Icon" class="w-8 h-8" />
381
- <div>
382
- <h2 class="font-semibold">Custom Header</h2>
383
- <p class="text-sm text-gray-600">Your custom content</p>
384
- </div>
385
- </div>
386
- </DrawerHeader>
387
- <div class="p-4">
388
- <p>Drawer content</p>
389
- </div>
390
- </DrawerContent>
391
- </Drawer>
254
+ <div id="my-portal"></div>
392
255
  ```
393
256
 
394
- #### DrawerFooter
257
+ ### Header and Footer
395
258
 
396
- ```svelte
397
- <script>
398
- import { Drawer, DrawerOverlay, DrawerContent, DrawerFooter } from '@abhivarde/svelte-drawer';
399
-
400
- let open = $state(false);
401
- </script>
402
-
403
- <!-- Simple footer with custom content -->
404
- <Drawer bind:open>
405
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
406
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col">
407
- <div class="p-4 flex-1">
408
- <h2>Drawer Content</h2>
409
- </div>
410
- <DrawerFooter>
411
- <button onclick={() => open = false} class="px-4 py-2 bg-gray-200 rounded">
412
- Cancel
413
- </button>
414
- <button class="px-4 py-2 bg-black text-white rounded">
415
- Confirm
416
- </button>
417
- </DrawerFooter>
418
- </DrawerContent>
419
- </Drawer>
259
+ Optional pre-styled components for quick layout setup.
420
260
 
421
- <!-- Custom footer with links -->
261
+ ```svelte
422
262
  <Drawer bind:open>
423
- <DrawerOverlay />
424
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col">
425
- <div class="p-4 flex-1">
426
- <h2>Content</h2>
427
- </div>
428
- <DrawerFooter class="bg-gray-50">
429
- <div class="flex justify-between w-full">
430
- <a href="/privacy" class="text-sm text-gray-600 hover:text-gray-900">Privacy</a>
431
- <a href="/terms" class="text-sm text-gray-600 hover:text-gray-900">Terms</a>
432
- </div>
433
- </DrawerFooter>
434
- </DrawerContent>
263
+ <DrawerOverlay />
264
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col h-[70vh]">
265
+ <DrawerHeader title="Settings" description="Manage your preferences" />
266
+ <div class="p-4 flex-1 overflow-y-auto">
267
+ <p>Content here</p>
268
+ </div>
269
+ <DrawerFooter>
270
+ <button onclick={() => open = false} class="px-4 py-2 bg-gray-200 rounded">Cancel</button>
271
+ <button class="px-4 py-2 bg-black text-white rounded">Save</button>
272
+ </DrawerFooter>
273
+ </DrawerContent>
435
274
  </Drawer>
436
275
  ```
437
276
 
438
- **Note:** These components are **optional**. You can still build custom headers and footers using plain HTML/Svelte markup without importing these components.
277
+ These are optional. You can use plain HTML instead.
439
278
 
440
279
  ### Persistent State
441
280
 
442
- Automatically save and restore drawer state across page reloads.
281
+ Saves and restores drawer state across page reloads.
443
282
 
444
283
  ```svelte
445
- <script>
446
- import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
447
-
448
- let open = $state(false);
449
- </script>
450
-
451
- <Drawer
452
- bind:open
453
- persistState={true}
454
- persistKey="main-drawer"
455
- >
284
+ <Drawer bind:open persistState={true} persistKey="settings-drawer">
456
285
  <DrawerOverlay />
457
- <DrawerContent class="...">
458
- <h2>This drawer remembers if it was open!</h2>
459
- <p>Reload the page and it will restore its state.</p>
286
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-6">
287
+ <DrawerHandle class="mb-8" />
288
+ <p>This drawer remembers if it was open.</p>
460
289
  </DrawerContent>
461
290
  </Drawer>
462
291
  ```
463
292
 
464
- **With snap points:**
293
+ With snap points:
465
294
 
466
295
  ```svelte
467
296
  <Drawer
@@ -472,228 +301,102 @@ Automatically save and restore drawer state across page reloads.
472
301
  persistKey="snap-drawer"
473
302
  persistSnapPoint={true}
474
303
  >
475
- <DrawerOverlay />
476
- <DrawerContent class="...">
477
- <h2>Position is saved too!</h2>
478
- <p>The snap point will be restored on reload.</p>
479
- </DrawerContent>
304
+ ...
480
305
  </Drawer>
481
306
  ```
482
307
 
483
- **Clear saved state programmatically:**
308
+ Clear saved state:
484
309
 
485
310
  ```svelte
486
311
  <script>
487
312
  import { clearDrawerState } from '@abhivarde/svelte-drawer';
488
-
489
- function resetDrawer() {
490
- clearDrawerState('main-drawer');
491
- // Drawer will reset to default state on next load
492
- }
493
- </script>
494
-
495
- <button onclick={resetDrawer}>Reset Drawer State</button>
496
- ```
497
-
498
- ### Auto Height (AI & Dynamic Content)
499
-
500
- Use `autoHeight` on `DrawerContent` when your drawer content changes height at runtime like AI streaming responses, multi-step forms, search results, or dynamic lists.
501
-
502
- ```svelte
503
- <script>
504
- import {
505
- Drawer,
506
- DrawerOverlay,
507
- DrawerContent,
508
- DrawerHandle
509
- } from '@abhivarde/svelte-drawer';
510
-
511
- let open = $state(false);
512
- let streaming = $state(false);
513
- let text = $state('');
514
-
515
- async function simulate() {
516
- open = true;
517
- streaming = true;
518
- text = '';
519
-
520
- const content =
521
- 'Sure! Here is what autoHeight does. It watches your content with a ResizeObserver. When content grows, the drawer follows automatically. No magic numbers, no hardcoded heights, it just works smoothly with streaming UI like AI chat apps.';
522
-
523
- for (let i = 0; i < content.length; i++) {
524
- text += content[i];
525
- await new Promise(r => setTimeout(r, 18));
526
- }
527
-
528
- streaming = false;
529
- }
530
313
  </script>
531
314
 
532
- <button onclick={simulate}>Ask AI</button>
533
-
534
- <Drawer bind:open>
535
- <DrawerOverlay />
536
-
537
- <DrawerContent
538
- autoHeight
539
- class="bg-gray-100 flex flex-col rounded-t-[10px] fixed bottom-0 left-0 right-0 outline-none"
540
- >
541
- <div class="p-4 bg-white rounded-t-[10px]">
542
- <DrawerHandle class="mb-8" />
543
-
544
- <p class="font-medium mb-2 text-gray-900">
545
- AI Response
546
- </p>
547
-
548
- {#if text}
549
- <p class="text-sm text-gray-600 leading-relaxed">
550
- {text}{streaming ? '▌' : ''}
551
- </p>
552
- {/if}
553
- </div>
554
- </DrawerContent>
555
- </Drawer>
315
+ <button onclick={() => clearDrawerState('settings-drawer')}>Reset</button>
556
316
  ```
557
317
 
558
- **How it works:**
559
-
560
- - `height: auto` is applied to the drawer when `autoHeight` is true
561
- - The drawer grows and shrinks naturally as content changes
562
- - Perfect for AI streaming, dynamic lists, forms, and async content
563
- - The slide animation remains smooth since it uses CSS transforms separately
564
- - Fully compatible with snap points, portals, and existing props
565
- - Zero impact on drawers that do not use `autoHeight` (opt-in, default `false`)
566
-
567
- ## Variants
318
+ ### Keyboard Shortcuts
568
319
 
569
- Available variants for `DrawerVariants` component:
320
+ - `Escape` to close (disable with `closeOnEscape={false}`)
321
+ - `Tab` and `Shift+Tab` to navigate focusable elements
322
+ - `Enter` or `Space` on overlay to close
570
323
 
571
- - `default` - Standard bottom drawer with gray background
572
- - `sheet` - iOS-style bottom sheet (white, rounded, 85vh height)
573
- - `dialog` - Center modal dialog style
574
- - `minimal` - Simple bottom drawer without extra styling
575
- - `sidebar` - Side navigation drawer (full height)
324
+ ### Focus Management
576
325
 
577
- ## Keyboard Shortcuts
578
-
579
- - **Escape** - Close the drawer (can be disabled with `closeOnEscape={false}`)
580
- - **Tab / Shift+Tab** - Navigate between focusable elements inside the drawer
581
- - **Enter / Space** (on overlay) - Close the drawer
326
+ The drawer auto-focuses the first focusable element when it opens, traps focus while open, and restores focus on close. Disable focus trapping with `trapFocus={false}` on `DrawerContent`.
582
327
 
583
328
  ## API Reference
584
329
 
585
330
  ### Drawer
586
331
 
587
- Main wrapper component that manages drawer state and animations.
588
-
589
- **Props:**
590
-
591
- - `open` (boolean, bindable) - Controls the open/closed state
592
- - `onOpenChange` (function, optional) - Callback when open state changes
593
- - `direction` ('bottom' | 'top' | 'left' | 'right', default: 'bottom') - Direction from which drawer slides
594
- - `closeOnEscape` (boolean, optional, default: true) - Whether Escape key closes the drawer
595
- - `closeThreshold` (number, optional, default: 0.3) - How far the user must drag to dismiss (0–1). Lower = easier to close, higher = requires a longer drag.
596
- - `snapPoints` (number[], optional) - Array of snap positions between 0-1
597
- - `activeSnapPoint` (number, bindable, optional) - Current active snap point value
598
- - `onSnapPointChange` (function, optional) - Callback fired when snap changes
599
- - `portal` (boolean, optional, default: false) - Render drawer in a portal
600
- - `portalContainer` (HTMLElement | string, optional) - Custom portal container element or selector
601
- - `persistState` (boolean, optional, default: false) - Enable persistent state
602
- - `persistKey` (string, optional, default: "default") - Unique identifier for this drawer
603
- - `persistSnapPoint` (boolean, optional, default: false) - Whether to persist snap point position
604
-
605
- ### DrawerOverlay
606
-
607
- Overlay component that appears behind the drawer.
608
-
609
- **Props:**
610
-
611
- - `class` (string, optional) - CSS classes for styling
612
- - `blur` (boolean | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl', optional) - Enable backdrop blur effect
332
+ | Prop | Type | Default | Description |
333
+ | ------------------- | -------------------------------- | --------- | ---------------------------------- |
334
+ | `open` | `boolean` | `false` | Controls open state (bindable) |
335
+ | `onOpenChange` | `(open: boolean) => void` | | Callback when open state changes |
336
+ | `direction` | `bottom \| top \| left \| right` | `bottom` | Slide-in edge |
337
+ | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
338
+ | `closeThreshold` | `number` | `0.3` | Drag distance to dismiss (0 to 1) |
339
+ | `animationDuration` | `number` | `220` | Animation duration in milliseconds |
340
+ | `animationEasing` | `(t: number) => number` | `expoOut` | Easing function for animations |
341
+ | `snapPoints` | `number[]` | | Snap positions between 0 and 1 |
342
+ | `activeSnapPoint` | `number` | | Current snap point (bindable) |
343
+ | `onSnapPointChange` | `(point: number) => void` | | Fires when snap point changes |
344
+ | `portal` | `boolean` | `false` | Render in a portal |
345
+ | `portalContainer` | `HTMLElement \| string` | | Custom portal target |
346
+ | `persistState` | `boolean` | `false` | Save state to localStorage |
347
+ | `persistKey` | `string` | `default` | Unique key for saved state |
348
+ | `persistSnapPoint` | `boolean` | `false` | Also save snap point position |
613
349
 
614
350
  ### DrawerContent
615
351
 
616
- Content container for the drawer.
352
+ | Prop | Type | Default | Description |
353
+ | ------------ | --------- | ------- | ---------------------------- |
354
+ | `class` | `string` | | CSS classes |
355
+ | `trapFocus` | `boolean` | `true` | Trap keyboard focus inside |
356
+ | `autoHeight` | `boolean` | `false` | Resize to fit content height |
617
357
 
618
- **Props:**
358
+ ### DrawerOverlay
619
359
 
620
- - `class` (string, optional) - CSS classes for styling
621
- - `trapFocus` (boolean, optional, default: true) - Whether to trap focus inside drawer
622
- - `autoHeight` (boolean, optional, default: false) - Automatically resize the drawer height to match its content
360
+ | Prop | Type | Default | Description |
361
+ | ------- | ----------------------------------------------- | ------- | ----------------------- |
362
+ | `class` | `string` | | CSS classes |
363
+ | `blur` | `boolean \| sm \| md \| lg \| xl \| 2xl \| 3xl` | | Backdrop blur intensity |
623
364
 
624
365
  ### DrawerHandle
625
366
 
626
- Visual drag indicator that automatically adapts to drawer direction.
627
-
628
- **Props:**
629
-
630
- - `class` (string, optional) - CSS classes for styling
631
-
632
- **Features:**
633
-
634
- - Automatically horizontal for `bottom`/`top` drawers (12px wide, 1.5px tall)
635
- - Automatically vertical for `left`/`right` drawers (1.5px wide, 12px tall)
636
- - Includes `data-drawer-drag` attribute for improved touch targeting
637
- - Fully customizable with Tailwind classes
638
-
639
- **Example:**
367
+ | Prop | Type | Default | Description |
368
+ | ------- | -------- | ------- | ----------- |
369
+ | `class` | `string` | | CSS classes |
640
370
 
641
- ```svelte
642
- <!-- Default gray handle -->
643
- <DrawerHandle class="mb-8" />
644
-
645
- <!-- Custom color -->
646
- <DrawerHandle class="bg-blue-500 mb-8" />
647
-
648
- <!-- Larger size -->
649
- <DrawerHandle class="w-16 h-2 mb-8" />
650
- ```
371
+ Automatically horizontal for `bottom` and `top` drawers, vertical for `left` and `right`.
651
372
 
652
373
  ### DrawerVariants
653
374
 
654
- Pre-styled drawer content with built-in variants.
655
-
656
- **Props:**
657
-
658
- - `variant` ('default' | 'sheet' | 'dialog' | 'minimal' | 'sidebar', default: 'default') - Preset style variant
659
- - `class` (string, optional) - Additional CSS classes for styling
660
- - `trapFocus` (boolean, optional, default: true) - Whether to trap focus inside drawer
375
+ | Prop | Type | Default | Description |
376
+ | ----------- | -------------------------------------------------- | --------- | -------------------------- |
377
+ | `variant` | `default \| sheet \| dialog \| minimal \| sidebar` | `default` | Preset style |
378
+ | `class` | `string` | | Additional CSS classes |
379
+ | `trapFocus` | `boolean` | `true` | Trap keyboard focus inside |
661
380
 
662
381
  ### DrawerHeader
663
382
 
664
- Optional pre-styled header component.
665
-
666
- **Props:**
667
-
668
- - `title` (string, optional) - Header title text
669
- - `description` (string, optional) - Description text below title
670
- - `showCloseButton` (boolean, optional, default: true) - Show close button
671
- - `onClose` (function, optional) - Custom close handler
672
- - `class` (string, optional) - CSS classes for styling
673
-
674
- **Features:**
675
-
676
- - Automatic close button with drawer context
677
- - Customizable via children render
678
- - Border and padding included
383
+ | Prop | Type | Default | Description |
384
+ | ----------------- | ------------ | ------- | ----------------------- |
385
+ | `title` | `string` | | Header title |
386
+ | `description` | `string` | | Description below title |
387
+ | `showCloseButton` | `boolean` | `true` | Show close button |
388
+ | `onClose` | `() => void` | | Custom close handler |
389
+ | `class` | `string` | | CSS classes |
679
390
 
680
391
  ### DrawerFooter
681
392
 
682
- Optional pre-styled footer component.
683
-
684
- **Props:**
685
-
686
- - `class` (string, optional) - CSS classes for styling
687
-
688
- **Features:**
689
-
690
- - Automatic border and spacing
691
- - Flexible content via children
692
- - Works with mt-auto for bottom positioning
393
+ | Prop | Type | Default | Description |
394
+ | ------- | -------- | ------- | ----------- |
395
+ | `class` | `string` | | CSS classes |
693
396
 
694
397
  ## Demo
695
398
 
696
- Visit [drawer.abhivarde.in](https://drawer.abhivarde.in) to see live examples.
399
+ [drawer.abhivarde.in](https://drawer.abhivarde.in)
697
400
 
698
401
  ## Star History
699
402
 
@@ -701,8 +404,7 @@ Visit [drawer.abhivarde.in](https://drawer.abhivarde.in) to see live examples.
701
404
 
702
405
  ## License
703
406
 
704
- This project is licensed under the MIT License.
705
- See the [LICENSE](./LICENSE) file for details.
407
+ MIT. See [LICENSE](./LICENSE) for details.
706
408
 
707
409
  ## Credits
708
410
 
@@ -3,7 +3,7 @@
3
3
  import { expoOut } from "svelte/easing";
4
4
  import { setContext, onMount } from "svelte";
5
5
  import DrawerPortal from "./DrawerPortal.svelte";
6
- import { saveDrawerState, loadDrawerState } from "../utils/storage"; // NEW
6
+ import { saveDrawerState, loadDrawerState } from "../utils/storage";
7
7
 
8
8
  let {
9
9
  open = $bindable(false),
@@ -19,6 +19,8 @@
19
19
  persistKey = "default",
20
20
  persistSnapPoint = false,
21
21
  closeThreshold = 0.3,
22
+ animationDuration = 220,
23
+ animationEasing = expoOut,
22
24
  children,
23
25
  } = $props();
24
26
 
@@ -27,10 +29,7 @@
27
29
  easing: expoOut,
28
30
  });
29
31
 
30
- let drawerPosition = new Tween(100, {
31
- duration: 220,
32
- easing: expoOut,
33
- });
32
+ let drawerPosition = new Tween(100);
34
33
 
35
34
  let previouslyFocusedElement: HTMLElement | null = null;
36
35
  let visible = false;
@@ -85,7 +84,10 @@
85
84
  previousSnapPoint !== activeSnapPoint
86
85
  ) {
87
86
  const snapPos = (1 - activeSnapPoint) * 100;
88
- drawerPosition.set(snapPos, { duration: 220 });
87
+ drawerPosition.set(snapPos, {
88
+ duration: animationDuration,
89
+ easing: animationEasing,
90
+ });
89
91
  }
90
92
  previousSnapPoint = activeSnapPoint;
91
93
  }
@@ -104,9 +106,15 @@
104
106
  activeSnapPoint = snapPoints[snapPoints.length - 1];
105
107
  }
106
108
  const snapPos = (1 - activeSnapPoint) * 100;
107
- drawerPosition.set(snapPos, { duration: 220 });
109
+ drawerPosition.set(snapPos, {
110
+ duration: animationDuration,
111
+ easing: animationEasing,
112
+ });
108
113
  } else {
109
- drawerPosition.set(0);
114
+ drawerPosition.set(0, {
115
+ duration: animationDuration,
116
+ easing: animationEasing,
117
+ });
110
118
  }
111
119
  } else if (visible) {
112
120
  if (
@@ -119,7 +127,10 @@
119
127
  }
120
128
 
121
129
  overlayOpacity.set(0, { duration: 120 });
122
- drawerPosition.set(100, { duration: 180 });
130
+ drawerPosition.set(100, {
131
+ duration: animationDuration,
132
+ easing: animationEasing,
133
+ });
123
134
 
124
135
  document.body.style.overflow = "";
125
136
 
@@ -131,7 +142,7 @@
131
142
  setTimeout(() => {
132
143
  visible = false;
133
144
  previousSnapPoint = undefined;
134
- }, 180);
145
+ }, animationDuration);
135
146
  }
136
147
  });
137
148
 
@@ -1,3 +1,4 @@
1
+ import { expoOut } from "svelte/easing";
1
2
  declare const Drawer: import("svelte").Component<{
2
3
  open?: boolean;
3
4
  onOpenChange?: any;
@@ -12,6 +13,8 @@ declare const Drawer: import("svelte").Component<{
12
13
  persistKey?: string;
13
14
  persistSnapPoint?: boolean;
14
15
  closeThreshold?: number;
16
+ animationDuration?: number;
17
+ animationEasing?: typeof expoOut;
15
18
  children: any;
16
19
  }, {}, "open" | "activeSnapPoint">;
17
20
  type Drawer = ReturnType<typeof Drawer>;
@@ -28,6 +28,9 @@
28
28
  let startPos = 0;
29
29
  let startDragPos = 0;
30
30
  let dragging = false;
31
+ let lastPointerPos = 0;
32
+ let lastPointerTime = 0;
33
+ let velocity = 0;
31
34
 
32
35
  function snapPointToPosition(snapPoint: number): number {
33
36
  return (1 - snapPoint) * 100;
@@ -115,6 +118,13 @@
115
118
  }
116
119
 
117
120
  drawer.drawerPosition.set(newPos, { duration: 0 });
121
+
122
+ const now = Date.now();
123
+ if (lastPointerTime > 0) {
124
+ velocity = (current - lastPointerPos) / (now - lastPointerTime);
125
+ }
126
+ lastPointerPos = current;
127
+ lastPointerTime = now;
118
128
  }
119
129
 
120
130
  function onPointerUp() {
@@ -123,7 +133,12 @@
123
133
  dragging = false;
124
134
 
125
135
  const pos = drawer.drawerPosition.current;
126
- const threshold = drawer.closeThreshold * 100;
136
+
137
+ const isFlick =
138
+ (drawer.direction === "bottom" && velocity > 0.5) ||
139
+ (drawer.direction === "top" && velocity < -0.5) ||
140
+ (drawer.direction === "left" && velocity < -0.5) ||
141
+ (drawer.direction === "right" && velocity > 0.5);
127
142
 
128
143
  if (drawer.snapPoints && drawer.snapPoints.length > 0) {
129
144
  const nearestSnapPoint = findNearestSnapPoint(pos);
@@ -132,20 +147,24 @@
132
147
  const lowestSnapPoint = Math.min(...drawer.snapPoints);
133
148
  const lowestSnapPos = snapPointToPosition(lowestSnapPoint);
134
149
 
135
- if (pos > lowestSnapPos + threshold) {
150
+ if (isFlick || pos > lowestSnapPos + 30) {
136
151
  drawer.closeDrawer();
137
152
  } else {
138
153
  drawer.drawerPosition.set(snapPos);
139
154
  drawer.setActiveSnapPoint?.(nearestSnapPoint);
140
155
  }
141
156
  } else {
142
- if (pos > threshold) {
157
+ if (isFlick || pos > 30) {
143
158
  drawer.closeDrawer();
144
159
  } else {
145
160
  drawer.drawerPosition.set(0);
146
161
  }
147
162
  }
148
163
 
164
+ velocity = 0;
165
+ lastPointerPos = 0;
166
+ lastPointerTime = 0;
167
+
149
168
  window.removeEventListener("pointermove", onPointerMove);
150
169
  window.removeEventListener("pointerup", onPointerUp);
151
170
  }
@@ -202,7 +221,8 @@
202
221
  <div
203
222
  bind:this={contentElement}
204
223
  class={className}
205
- style="transform: {getTransform()}; z-index: 50; touch-action: none;{autoHeight
224
+ data-svelte-drawer
225
+ style="transform: {getTransform()}; z-index: 50; touch-action: none; will-change: transform;{autoHeight
206
226
  ? ' height: auto;'
207
227
  : ''}"
208
228
  tabindex="-1"
@@ -214,3 +234,15 @@
214
234
  {@render children()}
215
235
  </div>
216
236
  {/if}
237
+
238
+ <style>
239
+ :global([data-svelte-drawer]::after) {
240
+ content: "";
241
+ position: absolute;
242
+ background: inherit;
243
+ left: 0;
244
+ right: 0;
245
+ height: 200px;
246
+ top: 100%;
247
+ }
248
+ </style>
package/dist/types.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface DrawerProps {
12
12
  persistKey?: string;
13
13
  persistSnapPoint?: boolean;
14
14
  closeThreshold?: number;
15
+ animationDuration?: number;
16
+ animationEasing?: (t: number) => number;
15
17
  }
16
18
  export interface DrawerContentProps {
17
19
  class?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abhivarde/svelte-drawer",
3
- "version": "1.0.7",
3
+ "version": "2.0.0",
4
4
  "description": "A drawer component for Svelte 5, inspired by Vaul",
5
5
  "author": "Abhi Varde",
6
6
  "license": "MIT",