# Drawer

Swipeable edge panel with accessible focus management and direct manipulation.

- Package: `@mielui/svelte`
- Component version: `1.0.1`
- Depends on Mielui components: _internal/utils, button, typography
- Shared utilities: components/_internal/surface, utils.cn, utils.pressable, utils.createContext

## Install

~~~~sh
pnpm dlx @mielui/svelte add drawer
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Body

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Close

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| element | HTMLButtonElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| onInteractOutside | InteractOutsideEventHandler \| undefined | - | No | No | Callback fired when an outside interaction event completes, which is either a `pointerup`, `mouseup`, or `touchend` event, depending on the user's input device. |
| interactOutsideBehavior | InteractOutsideBehaviorType \| undefined | - | No | No | Interact outside behavior type. `close`: Closes the element immediately. `defer-otherwise-close`: Delegates the action to the parent element. If no parent is found, it closes the element. `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done. `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to an outside interaction. |
| restoreScrollDelay | number \| null \| undefined | - | No | No | The delay in milliseconds before the scrollbar is restored after closing the dialog. This is only applicable when using the `child` snippet for custom transitions and `preventScroll` is `true`. You should set this to a value greater than the transition duration to prevent content from shifting during the transition. |
| onEscapeKeydown | ((e: KeyboardEvent) => void) \| undefined | - | No | No | Callback fired when escape is pressed. |
| escapeKeydownBehavior | EscapeBehaviorType \| undefined | - | No | No | Escape behavior type. `close`: Closes the element immediately. `defer-otherwise-close`: Delegates the action to its parent component that has an escape keydown handler. If no parent is found, it closes the element. `defer-otherwise-ignore`: Delegates the action to the parent element. If no parent is found, nothing is done. `ignore`: Prevents the element from closing and also blocks the parent element from closing in response to an escape key press. |
| forceMount | boolean \| undefined | - | No | No | Whether to force mount the component. |
| preventOverflowTextSelection | boolean \| undefined | - | No | No | Passing `true` will prevent the overflow of text selection outside the element, provided the element is the top layer. |
| onOpenAutoFocus | EventCallback \| undefined | - | No | No | Event handler called when auto-focusing on open. Can be prevented. |
| onCloseAutoFocus | EventCallback \| undefined | - | No | No | Event handler called when auto-focusing on close. Can be prevented. |
| trapFocus | boolean \| undefined | - | No | No | Whether focus is trapped within the focus scope. |
| surface | "solid" \| "glass" \| undefined | - | No | No | Surface treatment. Omit it to inherit `--mielui-surface` from the theme. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Description

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| element | HTMLParagraphElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Footer

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Handle

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Header

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Overlay

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| forceMount | boolean \| undefined | - | No | No | Whether to force mount the component. |
| element | HTMLDivElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Portal

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| to | PortalTarget \| undefined | - | No | No | Where to portal the content to. |
| disabled | boolean \| undefined | - | No | No | Disable portalling and render the component inline |
| children | Snippet<[]> \| undefined | - | No | No | The children content to render within the portal. |



### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| direction | "top" \| "bottom" \| "left" \| "right" \| undefined | - | No | No | Direction of the drawer. Can be `top` or `bottom`, `left`, `right`. |
| open | boolean \| undefined | false | No | Yes | Whether it is open. Bind it to control the state from outside. |
| onOpenChange | ((open: boolean) => void) \| undefined | - | No | No | Called with the new state whenever it opens or closes. |
| closeThreshold | number \| undefined | - | No | No | Number between 0 and 1 that determines when the drawer should be closed. Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more. |
| handleOnly | boolean \| undefined | - | No | No | When `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component. |
| dismissible | boolean \| undefined | true | No | No | When `false` dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in combination with the `open` prop, otherwise you won't be able to open/close the drawer. |
| repositionInputs | boolean \| undefined | - | No | No | When `true` Vaul will reposition inputs rather than scroll then into view if the keyboard is in the way. Setting it to `false` will fall back to the default browser behavior. |
| nested | boolean \| undefined | false | No | No | Set it on a drawer that opens from inside another drawer. |



### Title

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| level | 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| undefined | - | No | No | The heading level of the dialog title. |
| element | HTMLHeadingElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Trigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| id | string \| undefined | - | No | No | Id of the element. |
| style | string \| StyleProperties \| null \| undefined | - | No | No | Inline styles for the element. |
| element | HTMLButtonElement \| null \| undefined | null | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { Drawer as Primitive } from 'vaul-svelte';

export { default as Body } from './drawer-body.svelte';
export { default as Close } from './drawer-close.svelte';
export { default as Content } from './drawer-content.svelte';
export { default as Description } from './drawer-description.svelte';
export { default as Footer } from './drawer-footer.svelte';
export { default as Handle } from './drawer-handle.svelte';
export { default as Header } from './drawer-header.svelte';
export { default as Overlay } from './drawer-overlay.svelte';
export { default as Portal } from './drawer-portal.svelte';
export { default as Root } from './drawer-root.svelte';
export { default as Title } from './drawer-title.svelte';
export { default as Trigger } from './drawer-trigger.svelte';
export type DrawerRootProps = Pick<
    Primitive.RootProps,
    | 'children'
    | 'open'
    | 'onOpenChange'
    | 'direction'
    | 'dismissible'
    | 'handleOnly'
    | 'closeThreshold'
    | 'repositionInputs'
> & { nested?: boolean };
export type DrawerTriggerProps = Omit<Primitive.TriggerProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLButtonElement | null;
};
export type DrawerCloseProps = Omit<Primitive.CloseProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLButtonElement | null;
};
export type DrawerTitleProps = Omit<Primitive.TitleProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLHeadingElement | null;
};
export type DrawerDescriptionProps = Omit<Primitive.DescriptionProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLParagraphElement | null;
};
export type DrawerContentProps = Omit<
    Primitive.ContentProps,
    'child' | 'ref' | 'onFocusOutside'
> & {
    /** Surface treatment. Omit it to inherit `--mielui-surface` from the theme. */
    surface?: 'solid' | 'glass';
    /** Bindable reference to the DOM element. */
    element?: HTMLDivElement | null;
};
export type DrawerOverlayProps = Omit<Primitive.OverlayProps, 'child' | 'ref'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLDivElement | null;
};
export type DrawerHandleProps = Omit<Primitive.HandleProps, 'child' | 'ref' | 'preventCycle'> & {
    /** Bindable reference to the DOM element. */
    element?: HTMLDivElement | null;
};
export type DrawerPortalProps = Primitive.PortalProps;
export type DrawerRegionProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /** Content rendered inside. */
    children?: Snippet;
    /** Bindable reference to the DOM element. */
    element?: HTMLDivElement | null;
};
~~~~

## Examples

### Dismissal

~~~~svelte
<script lang="ts">
    import * as Drawer from '@mielui/svelte/components/drawer';

    let blocked = $state(0);
</script>

<Drawer.Root>
    <Drawer.Trigger>Open drawer</Drawer.Trigger>
    <Drawer.Portal>
        <Drawer.Overlay />
        <Drawer.Content
            interactOutsideBehavior="ignore"
            onEscapeKeydown={(event) => {
                event.preventDefault();
                blocked += 1;
            }}
        >
            <Drawer.Header>
                <Drawer.Title>Unsaved changes</Drawer.Title>
                <Drawer.Description>
                    Outside presses are ignored. Escape presses blocked: {blocked}.
                </Drawer.Description>
            </Drawer.Header>
            <Drawer.Footer>
                <Drawer.Close>Discard and close</Drawer.Close>
            </Drawer.Footer>
        </Drawer.Content>
    </Drawer.Portal>
</Drawer.Root>
~~~~

### Glass

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Drawer from '@mielui/svelte/components/drawer';
    import { Switch } from '@mielui/svelte/components/switch';

    let open = $state(false);
    let allowComments = $state(false);
    let notifyFollowers = $state(true);
    let shared = $state('');

    function share() {
        const extras = [
            allowComments ? 'comments on' : 'comments off',
            notifyFollowers ? 'followers notified' : 'followers not notified'
        ];
        shared = `Reading list shared with ${extras.join(', ')}.`;
        open = false;
    }
</script>

<div class="flex flex-col items-center gap-3">
    <Drawer.Root bind:open>
        <Drawer.Trigger>Share reading list</Drawer.Trigger>
        <Drawer.Portal>
            <Drawer.Overlay />
            <Drawer.Content surface="glass">
                <Drawer.Handle />
                <Drawer.Header>
                    <Drawer.Title>Share reading list</Drawer.Title>
                    <Drawer.Description>
                        Choose how others can follow Weekend reading.
                    </Drawer.Description>
                </Drawer.Header>
                <Drawer.Body class="flex flex-col gap-4 py-6">
                    <Switch bind:checked={allowComments} label="Allow comments" />
                    <Switch bind:checked={notifyFollowers} label="Notify followers" />
                </Drawer.Body>
                <Drawer.Footer>
                    <Drawer.Close>Cancel</Drawer.Close>
                    <Button onclick={share}>Share</Button>
                </Drawer.Footer>
            </Drawer.Content>
        </Drawer.Portal>
    </Drawer.Root>

    {#if shared}
        <p role="status" class="text-sm text-foreground-muted">{shared}</p>
    {/if}
</div>
~~~~

### Hero

~~~~svelte
<script lang="ts">
    import { numberShuffle } from '@mielui/svelte/actions/number-shuffle';
    import { Button } from '@mielui/svelte/components/button';
    import * as Drawer from '@mielui/svelte/components/drawer';
    import { Slider } from '@mielui/svelte/components/slider';
    import { Switch } from '@mielui/svelte/components/switch';

    let open = $state(false);
    let textSize = $state(18);
    let comfortableSpacing = $state(true);
    let saved = $state({
        textSize: 18,
        comfortableSpacing: true
    });

    function restore() {
        textSize = saved.textSize;
        comfortableSpacing = saved.comfortableSpacing;
    }

    function save() {
        saved = {
            textSize,
            comfortableSpacing
        };
        open = false;
    }
</script>

<div class="flex flex-col items-center gap-3">
    <Drawer.Root
        bind:open
        handleOnly
        onOpenChange={(next) => {
            if (!next) {
                restore();
            }
        }}
    >
        <Drawer.Trigger>Reading preferences</Drawer.Trigger>
        <Drawer.Portal>
            <Drawer.Overlay />
            <Drawer.Content>
                <Drawer.Handle />
                <Drawer.Header>
                    <Drawer.Title>Make yourself comfortable</Drawer.Title>
                    <Drawer.Description>
                        Adjust the text to suit the way you read.
                    </Drawer.Description>
                </Drawer.Header>
                <Drawer.Body class="flex flex-col gap-6 py-6">
                    <div class="flex flex-col gap-3" data-vaul-no-drag>
                        <div class="flex items-center justify-between text-sm">
                            <span>Text size</span>
                            <span class="tabular-nums text-foreground-muted">
                                <span use:numberShuffle={{ value: textSize }}>{textSize}</span>
                                px
                            </span>
                        </div>
                        <Slider
                            bind:value={textSize}
                            min={14}
                            max={24}
                            step={1}
                            label="Text size"
                        />
                    </div>
                    <Switch bind:checked={comfortableSpacing} label="Comfortable spacing" />
                    <p
                        class="rounded-[var(--radius-xl)] border border-border bg-card p-4 text-foreground-muted"
                        style:font-size={`${textSize}px`}
                        style:line-height={comfortableSpacing ? 1.8 : 1.4}
                    >
                        A quiet place to pause, find a new idea, and enjoy the next page.
                    </p>
                </Drawer.Body>
                <Drawer.Footer>
                    <Drawer.Close>Cancel</Drawer.Close>
                    <Button onclick={save}>Save</Button>
                </Drawer.Footer>
            </Drawer.Content>
        </Drawer.Portal>
    </Drawer.Root>

    <p role="status" class="text-sm text-foreground-muted">
        {`Saved: ${saved.textSize} px text, ${saved.comfortableSpacing ? 'comfortable' : 'compact'} spacing.`}
    </p>
</div>
~~~~

### Nested

~~~~svelte
<script lang="ts">
    import * as Drawer from '@mielui/svelte/components/drawer';
</script>

<Drawer.Root>
    <Drawer.Trigger>Review changes</Drawer.Trigger>
    <Drawer.Portal>
        <Drawer.Overlay />
        <Drawer.Content>
            <Drawer.Handle />
            <Drawer.Header>
                <Drawer.Title>Review changes</Drawer.Title>
                <Drawer.Description>
                    Three updates are ready to apply to your reading list.
                </Drawer.Description>
            </Drawer.Header>
            <Drawer.Body>
                <dl class="mb-5 flex flex-col gap-3 text-sm">
                    <div class="flex justify-between gap-4">
                        <dt class="text-foreground-muted">Collection</dt>
                        <dd>Weekend reading</dd>
                    </div>
                    <div class="flex justify-between gap-4">
                        <dt class="text-foreground-muted">Articles added</dt>
                        <dd class="tabular-nums">2</dd>
                    </div>
                    <div class="flex justify-between gap-4">
                        <dt class="text-foreground-muted">Articles archived</dt>
                        <dd class="tabular-nums">1</dd>
                    </div>
                </dl>
                <Drawer.Root nested>
                    <Drawer.Trigger>Review details</Drawer.Trigger>
                    <Drawer.Portal>
                        <Drawer.Overlay />
                        <Drawer.Content>
                            <Drawer.Handle />
                            <Drawer.Header>
                                <Drawer.Title>Change details</Drawer.Title>
                                <Drawer.Description>
                                    Review the articles included in this update.
                                </Drawer.Description>
                            </Drawer.Header>
                            <Drawer.Body>
                                <ul class="flex flex-col gap-3 text-sm">
                                    <li class="flex justify-between gap-4">
                                        <span>The art of paying attention</span>
                                        <span class="text-foreground-muted">Added</span>
                                    </li>
                                    <li class="flex justify-between gap-4">
                                        <span>A slower kind of weekend</span>
                                        <span class="text-foreground-muted">Added</span>
                                    </li>
                                    <li class="flex justify-between gap-4">
                                        <span>Notes from the city</span>
                                        <span class="text-foreground-muted">Archived</span>
                                    </li>
                                </ul>
                            </Drawer.Body>
                            <Drawer.Footer>
                                <Drawer.Close>Back to review</Drawer.Close>
                            </Drawer.Footer>
                        </Drawer.Content>
                    </Drawer.Portal>
                </Drawer.Root>
            </Drawer.Body>
            <Drawer.Footer>
                <Drawer.Close>Finish review</Drawer.Close>
            </Drawer.Footer>
        </Drawer.Content>
    </Drawer.Portal>
</Drawer.Root>
~~~~

### Sides

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Drawer from '@mielui/svelte/components/drawer';
    import { Switch } from '@mielui/svelte/components/switch';

    type Direction = 'top' | 'right' | 'bottom' | 'left';

    const directions: Direction[] = ['top', 'right', 'bottom', 'left'];
    let open = $state(false);
    let direction = $state<Direction>('right');
    let unreadOnly = $state(true);
    let showArchived = $state(false);

    function show(next: Direction) {
        direction = next;
        open = true;
    }
</script>

<div class="flex flex-wrap justify-center gap-2">
    {#each directions as option (option)}
        <Button variant="secondary" onclick={() => show(option)}>
            {option[0].toUpperCase() + option.slice(1)}
        </Button>
    {/each}
</div>

<Drawer.Root bind:open {direction}>
    <Drawer.Portal>
        <Drawer.Overlay />
        <Drawer.Content>
            <Drawer.Handle />
            <Drawer.Header>
                <Drawer.Title>Filters</Drawer.Title>
                <Drawer.Description>Choose what appears in your reading list.</Drawer.Description>
            </Drawer.Header>
            <Drawer.Body class="flex flex-col gap-4 py-6">
                <Switch bind:checked={unreadOnly} label="Unread only" />
                <Switch bind:checked={showArchived} label="Show archived" />
            </Drawer.Body>
            <Drawer.Footer>
                <Drawer.Close>Close</Drawer.Close>
                <Button
                    onclick={() => {
                        open = false;
                    }}
                >
                    Apply
                </Button>
            </Drawer.Footer>
        </Drawer.Content>
    </Drawer.Portal>
</Drawer.Root>
~~~~

For the rendered reference, visit [/docs/components/drawer](/docs/components/drawer).
