The best drawer on the planet — for Svelte 5. Velocity-based, runes-first, unstyled: drag-to-dismiss, snap points, nested drawers, an accessible dialog core. Zero runtime dependencies. Fling it, catch it mid-close, reverse it on a dime — joy to use. (See Why svaul?.)
npm i @harshmandan/svaulRequires Svelte
^5.
<script lang="ts">
import Drawer from "@harshmandan/svaul";
let open = $state(false);
</script>
<Drawer bind:open class="rounded-t-xl bg-white p-4">
{#snippet trigger(props)}
<button {...props}>Open</button>
{/snippet}
<h2>A bottom sheet</h2>
<p>Drag it down to dismiss, or tap outside.</p>
{#snippet footer({ close })}
<button onclick={close}>Close</button>
{/snippet}
</Drawer>Every part of the drawer is customizable via named snippets in the <Drawer> component:
trigger, overlay, content, handle, title, description,
header, footer, and the default children (the scrollable body). Supply only the parts
you want to own; each renders a sensible default otherwise. Body snippets receive
{ open, close, setOpen, closeAll, cycleSnapPoint } controls.
To close the drawer on the browser back button, drive open yourself and reset it from
onOpenChange + a popstate listener (svaul intentionally doesn't touch history).
| Prop | Default | |
|---|---|---|
bind:open |
— | controlled/uncontrolled open state (also onOpenChange) |
direction |
"bottom" |
top · bottom · left · right |
snapPoints |
— | fractions (0.5) or px ("148px"); pair with bind:activeSnapPoint |
dismissible |
true |
drag / overlay-click / Escape can close |
modal |
true |
overlay + scroll-lock + focus-trap |
scaleBackground |
false |
scale [data-svaul-drawer-wrapper] (the card-stack look) |
handleOnly |
false |
only the handle initiates a drag |
dragSensitivity |
1 |
>1 makes the drawer move faster than the cursor |
disableAnimation |
false |
instant open / close |
Plus closeThreshold, repositionInputs, preventScrollRestoration, noBodyStyles,
setBackgroundColorOnScale, backgroundColor, borderRadius, autoFocus,
fadeFromIndex, snapToSequentialPoint, container, keepMounted, onlyPrimaryPointer
— see the exported types.
Snap points accept fractions (≤ 1, e.g. 0.5), pixels ("148px" or a plain number > 1),
percentages ("50%"), rem/vh/vw, and
calc() combinations: snapPoints={["calc(50% + 24px)", 1]}.
It ships unstyled — bring your own class. Everything is reachable via data attributes
([data-svaul-drawer], [data-svaul-drawer-overlay], [data-svaul-drawer-handle]) and CSS variables
(--svaul-drawer-bg, --svaul-drawer-overlay-bg, --svaul-drawer-handle-bg, --svaul-drawer-handle-gap, …).
The library's own CSS lives in the svaul cascade layer, so your styles always win
without !important. With Tailwind v4, declare the layer order once:
@layer svaul;
@import "tailwindcss";
scaleBackgroundnote: wrap your page in<div data-svaul-drawer-wrapper>and give it an opaque background. The gap behind the lifted page is painted viasetBackgroundColorOnScale(default black) — if the wrapper is transparent the whole page looks black.
- Svelte 5 runes-native — built on
$state/$derived/$effect,{@attach}attachments and snippets, not stores or$:side-effect chains. - Zero runtime dependencies — the portal, focus-trap, scroll-lock, dismiss/Escape,
inertbackground and ARIA are all hand-rolled. (vaul-svelte shipsbits-ui; vaul is React-only.) - Svelte Native API — a single
<Drawer>with named snippets and a headlessDrawerclass, instead of compoundDrawer.Root/Content/…parts. - Bug fixes Fixes bugs with both vaul and svelte-vaul: ref-counted scroll-lock & background-color
restore that always reverts, topmost-only outside-click for nested drawers, the
on-screen-keyboard "drawer shoots off-screen" fix, pixel-snapped snap offsets (no blurry
text), a dismiss-blink fix,
prefers-reduced-motionhonored on the scaled background, andmodalactually wired through. And more.
svaul v1 was inspired by vaul and vaul-svelte. v2 reimagines the motion system but the look is still from vaul. Thanks to both ♥.
MIT