https://github.com/harshmandan/svaul
Svelte 5 port of the amazing Vaul bottom drawer
https://github.com/harshmandan/svaul
component drawer svelte sveltekit
Last synced: about 1 month ago
JSON representation
Svelte 5 port of the amazing Vaul bottom drawer
- Host: GitHub
- URL: https://github.com/harshmandan/svaul
- Owner: harshmandan
- License: mit
- Created: 2026-06-05T11:56:06.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-07T18:07:19.000Z (about 2 months ago)
- Last Synced: 2026-06-14T05:33:48.477Z (about 1 month ago)
- Topics: component, drawer, svelte, sveltekit
- Language: TypeScript
- Homepage: https://harshmandan.github.io/svaul/
- Size: 108 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README

svaul
[](https://www.npmjs.com/package/@harshmandan/svaul)
[](https://www.npmjs.com/package/@harshmandan/svaul)
An unstyled, **runes-first drawer / bottom-sheet for Svelte 5** — drag-to-dismiss, snap
points, nested drawers, and an accessible dialog core. **Zero runtime dependencies.**
A ground-up Svelte 5 re-imagining of [vaul](https://github.com/emilkowalski/vaul) and
[vaul-svelte](https://github.com/huntabyte/vaul-svelte) (see [Credits](#credits)). See
the advantages over both in the [Why svaul?](#why-svaul) section below.
## Quick start
```sh
npm i @harshmandan/svaul
```
> Requires Svelte `^5`.
```svelte
import Drawer from "@harshmandan/svaul";
let open = $state(false);
{#snippet trigger(props)}
Open
{/snippet}
A bottom sheet
Drag it down to dismiss, or tap outside.
{#snippet footer({ close })}
Close
{/snippet}
```
Every part of the drawer is customizable via **named snippets** in the `` 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).
## Key props
| 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-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 (`0.5`), pixels (`"148px"`), percentages (`"50%"`), and
`calc()` combinations: `snapPoints={["calc(50% + 24px)", 1]}`.
## Styling
It ships unstyled — bring your own `class`. Everything is reachable via data attributes
(`[data-drawer]`, `[data-drawer-overlay]`, `[data-drawer-handle]`) and CSS variables
(`--drawer-bg`, `--drawer-overlay-bg`, `--drawer-handle-bg`, `--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:
```css
@layer svaul;
@import "tailwindcss";
```
> **`scaleBackground` note:** wrap your page in `
` and give it an
> opaque background. The gap behind the lifted page is painted via `setBackgroundColorOnScale`
> (default black) — if the wrapper is transparent the whole page looks black.
## Why svaul?
- **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, `inert`
background and ARIA are all hand-rolled. (vaul-svelte ships `bits-ui`; vaul is React-only.)
- **Svelte Native API** — a single `` with named snippets *and* a headless `Drawer` class,
instead of compound `Drawer.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-motion` honored on the scaled background, and
`modal` actually wired through. And more.
## Credits
This library stands entirely on the shoulders of two projects:
- **[vaul](https://github.com/emilkowalski/vaul)** by [Emil Kowalski](https://emilkowalski.com)
— the original React drawer. Its drag physics, snap-point math, and the overall feel are
ported from here.
- **[vaul-svelte](https://github.com/huntabyte/vaul-svelte)** by
[Huntabyte](https://github.com/huntabyte) — the Svelte port that proved the idea. `svaul`
is a from-scratch Svelte 5 (runes, zero-dependency) take on the same concept.
Huge thanks to both. ♥
## License
MIT