@01-collective/drawer-svelte
v0.1.0
Published
Svelte 5 drawer/bottom-sheet component with native drag gestures, inspired by Vaul
Downloads
19
Maintainers
Readme
@01-collective/drawer-svelte
A Svelte 5 drawer component — inspired by Vaul. A drawer/bottom-sheet component with native-feeling drag gestures. Built on top of Bits UI Dialog.
Installation
npm install @01-collective/drawer-svelteQuick Start
Wrap your app content in a data-svelte-drawer-drawer-wrapper element (typically in your root layout or app.html):
<body>
<div data-svelte-drawer-drawer-wrapper="">
%sveltekit.body%
</div>
</body>Then use the drawer:
<script>
import { Drawer } from '@01-collective/drawer-svelte';
</script>
<Drawer.Root>
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay class="overlay" />
<Drawer.Content class="content">
<Drawer.Handle />
<Drawer.Title>Title</Drawer.Title>
<Drawer.Description>Description</Drawer.Description>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>Features
- Drag to dismiss — flick or drag past threshold to close
- Snap points — multiple stop positions (fraction or px)
- All directions —
bottom,top,left,right - Nested drawers — drawers within drawers
- Background scaling — iOS-style sheet effect
- Keyboard handling — repositions inputs when virtual keyboard opens
- Mobile scroll lock — prevents body scroll on iOS Safari
- Modal & non-modal — with proper focus trapping
- Accessible — built on Bits UI Dialog (WAI-ARIA compliant)
- Headless — unstyled, bring your own CSS
Components
| Component | Description |
|-----------|-------------|
| Drawer.Root | State manager, wraps everything |
| Drawer.Trigger | Button that opens the drawer |
| Drawer.Portal | Renders content in a portal |
| Drawer.Overlay | Backdrop behind the drawer |
| Drawer.Content | The drawer panel (handles drag events) |
| Drawer.Handle | Drag handle with snap point cycling |
| Drawer.Close | Button that closes the drawer |
| Drawer.Title | Accessible title |
| Drawer.Description | Accessible description |
| Drawer.NestedRoot | Root for nested drawers |
Props
Drawer.Root
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | false | Controlled open state (bindable) |
| onOpenChange | (open: boolean) => void | — | Called when open state changes |
| direction | 'top' \| 'bottom' \| 'left' \| 'right' | 'bottom' | Drawer direction |
| snapPoints | (number \| string)[] | — | Snap points as fractions (0-1) or px strings |
| fadeFromIndex | number | last snap point | Overlay fades from this snap point index |
| activeSnapPoint | number \| string \| null | — | Controlled active snap point |
| setActiveSnapPoint | (point) => void | — | Callback when snap point changes |
| closeThreshold | number | 0.25 | Fraction of height to trigger close |
| dismissible | boolean | true | Whether the drawer can be closed |
| modal | boolean | true | Modal mode with focus trap |
| handleOnly | boolean | false | Only allow dragging via Handle |
| shouldScaleBackground | boolean | false | Scale background on open |
| nested | boolean | false | Is this a nested drawer |
| noBodyStyles | boolean | false | Don't apply body styles |
| fixed | boolean | false | Only change height on keyboard open |
| autoFocus | boolean | false | Auto-focus content on open |
| onDrag | (event, progress) => void | — | Called during drag |
| onRelease | (event, open) => void | — | Called on drag release |
| onClose | () => void | — | Called when drawer closes |
| onAnimationEnd | (open: boolean) => void | — | Called after open/close animation |
Examples
Snap Points
<Drawer.Root snapPoints={[0.25, 0.5, 1]} fadeFromIndex={1}>
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.Handle />
<p>Drag to snap points at 25%, 50%, and 100%</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>Right Drawer
<Drawer.Root direction="right">
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<p>Slides in from the right</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>Nested Drawers
<Drawer.Root>
<Drawer.Trigger>Open Outer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.NestedRoot>
<Drawer.Trigger>Open Inner</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<p>Nested drawer content</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.NestedRoot>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>Background Scaling
<Drawer.Root shouldScaleBackground>
<!-- ... -->
</Drawer.Root>Controlled State
<script>
let open = $state(false);
</script>
<button onclick={() => open = true}>Open programmatically</button>
<Drawer.Root bind:open>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<p>Controlled drawer</p>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>Prevent Closing
<Drawer.Root dismissible={false}>
<!-- User cannot drag-to-close or click overlay to close -->
</Drawer.Root>Non-modal
<Drawer.Root modal={false}>
<!-- No overlay, no focus trap, interact with page behind -->
</Drawer.Root>Styling
The component is headless. Style using classes, inline styles, or data attributes:
[data-svelte-drawer-overlay] {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
}
[data-svelte-drawer-drawer] {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-radius: 10px 10px 0 0;
}Data Attributes
| Attribute | Values | Element |
|-----------|--------|---------|
| data-svelte-drawer-drawer | — | Content |
| data-svelte-drawer-drawer-direction | top\|bottom\|left\|right | Content |
| data-svelte-drawer-overlay | — | Overlay |
| data-svelte-drawer-handle | — | Handle |
| data-svelte-drawer-snap-points | true\|false | Content, Overlay |
| data-svelte-drawer-drawer-visible | true\|false | Handle |
| data-svelte-drawer-no-drag | — | Any element (prevents drag) |
| data-state | open\|closed | Content, Overlay |
Credits
- Svelte Drawer by Emil Kowalski — original React implementation
- Bits UI — accessible Svelte 5 primitives
License
MIT
