npm package: @01-collective/drawer-svelte
Description: Svelte 5 drawer/bottom-sheet component with native drag gestures, inspired by Vaul
Latest version: 0.1.0

# @01-collective/drawer-svelte

A Svelte 5 drawer component — inspired by [Vaul](https://github.com/emilkowalski/vaul). A drawer/bottom-sheet component with native-feeling drag gestures. Built on top of [Bits UI](https://bits-ui.com) Dialog.

## Installation

```bash
npm install @01-collective/drawer-svelte
```

## Quick Start

Wrap your app content in a `data-svelte-drawer-drawer-wrapper` element (typically in your root layout or `app.html`):

```html
<body>
  <div data-svelte-drawer-drawer-wrapper="">
    %sveltekit.body%
  </div>
</body>
```

Then use the drawer:

```svelte
<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

```svelte
<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

```svelte
<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

```svelte
<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

```svelte
<Drawer.Root shouldScaleBackground>
  <!-- ... -->
</Drawer.Root>
```

### Controlled State

```svelte
<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

```svelte
<Drawer.Root dismissible={false}>
  <!-- User cannot drag-to-close or click overlay to close -->
</Drawer.Root>
```

### Non-modal

```svelte
<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:

```css
[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](https://github.com/emilkowalski/svelte-drawer) by Emil Kowalski — original React implementation
- [Bits UI](https://bits-ui.com) — accessible Svelte 5 primitives

## License

MIT