Overlay

Drawer

A slide-out panel with 4 directions, inset mode, drag handle, header/body/footer sections, and snap points. Built on vaul-svelte.

Basic Usage

Use bind:open to control visibility.

<script lang="ts">
  import { Drawer, Button } from 'sv5ui';

  let open = $state(false);
</script>

<Drawer bind:open title="Basic Drawer" description="This is a simple drawer example.">
  <Button label="Open Drawer" onclick={() => (open = true)} />

  {#snippet body()}
    <p>This is the drawer body content.</p>
  {/snippet}
</Drawer>

Directions

Slide from any edge: bottom, left, right, top.

<Drawer bind:open direction="bottom" title="Bottom Drawer">
  <Button label="Bottom" onclick={() => (open = true)} />
  {#snippet body()}
    <p>Slides in from the bottom.</p>
  {/snippet}
</Drawer>

<Drawer bind:open direction="left" title="Left Drawer">
  <Button label="Left" onclick={() => (open = true)} />
  {#snippet body()}
    <p>Slides in from the left.</p>
  {/snippet}
</Drawer>

<Drawer bind:open direction="right" title="Right Drawer">
  <Button label="Right" onclick={() => (open = true)} />
  {#snippet body()}
    <p>Slides in from the right.</p>
  {/snippet}
</Drawer>

<Drawer bind:open direction="top" title="Top Drawer">
  <Button label="Top" onclick={() => (open = true)} />
  {#snippet body()}
    <p>Slides in from the top.</p>
  {/snippet}
</Drawer>

Inset

Rounded corners with padding from edges.

<Drawer bind:open inset title="Inset Drawer" description="Rounded corners with padding from edges.">
  <Button label="Open Inset Drawer" onclick={() => (open = true)} />

  {#snippet body()}
    <p>This drawer has inset styling with rounded corners.</p>
  {/snippet}
</Drawer>

With Footer

Add actions in the footer.

<Drawer bind:open title="Confirm Action" description="Are you sure you want to proceed?">
  <Button label="Open Drawer" onclick={() => (open = true)} />

  {#snippet body()}
    <p>This action cannot be undone. Please review before confirming.</p>
  {/snippet}

  {#snippet footer()}
    <div class="flex gap-2 justify-end">
      <Button variant="outline" label="Cancel" onclick={() => (open = false)} />
      <Button label="Confirm" onclick={() => (open = false)} />
    </div>
  {/snippet}
</Drawer>

No Handle

Hide the drag handle.

<Drawer bind:open handle={false} title="No Handle" description="This drawer has no drag handle.">
  <Button label="Open Drawer" onclick={() => (open = true)} />

  {#snippet body()}
    <p>The drag handle is hidden.</p>
  {/snippet}
</Drawer>

Nested Drawers

Stack drawers using the nested prop on the inner drawer.

<script lang="ts">
  import { Drawer, Button } from 'sv5ui';

  let outerOpen = $state(false);
  let innerOpen = $state(false);
</script>

<Drawer bind:open={outerOpen} title="Settings" description="Manage your preferences.">
  <Button label="Open Settings" onclick={() => (outerOpen = true)} />

  {#snippet body()}
    <div class="space-y-4">
      <p>Main settings panel.</p>
      <!-- Nested drawer must be inside the outer drawer -->
      <Drawer bind:open={innerOpen} nested title="Advanced Settings">
        <Button label="Advanced Settings" variant="outline" onclick={() => (innerOpen = true)} />

        {#snippet body()}
          <p>This is a nested drawer on top of the outer one.</p>
        {/snippet}

        {#snippet footer()}
          <Button label="Back" onclick={() => (innerOpen = false)} class="w-full" />
        {/snippet}
      </Drawer>
    </div>
  {/snippet}
</Drawer>

With Form & SelectMenu

A right-side drawer with a full contact form including searchable SelectMenu.

<script lang="ts">
  import { Drawer, Button, Input, Textarea, Select, SelectMenu, FormField } from 'sv5ui';

  let open = $state(false);
</script>

<Drawer bind:open direction="right" title="New Contact" description="Add a new contact to your list.">
  <Button label="Add Contact" onclick={() => (open = true)} />

  {#snippet body()}
    <form class="space-y-4">
      <FormField label="Name" required>
        <Input placeholder="Full name" />
      </FormField>

      <FormField label="Email" required>
        <Input type="email" placeholder="email@example.com" leadingIcon="lucide:mail" />
      </FormField>

      <FormField label="Company">
        <SelectMenu
          placeholder="Search companies..."
          items={[
            { value: 'google', label: 'Google', icon: 'logos:google-icon' },
            { value: 'apple', label: 'Apple', icon: 'logos:apple' },
            { value: 'microsoft', label: 'Microsoft', icon: 'logos:microsoft-icon' },
            { value: 'meta', label: 'Meta', icon: 'logos:meta-icon' }
          ]}
        />
      </FormField>

      <FormField label="Role">
        <Select placeholder="Select role" items={[
          { value: 'ceo', label: 'CEO' },
          { value: 'cto', label: 'CTO' },
          { value: 'designer', label: 'Designer' },
          { value: 'engineer', label: 'Engineer' }
        ]} />
      </FormField>

      <FormField label="Notes" hint="Optional">
        <Textarea placeholder="Additional notes..." autoresize />
      </FormField>
    </form>
  {/snippet}

  {#snippet footer()}
    <div class="flex w-full justify-end gap-2">
      <Button variant="outline" label="Cancel" onclick={() => (open = false)} />
      <Button label="Save Contact" onclick={() => (open = false)} />
    </div>
  {/snippet}
</Drawer>

Non-Dismissible

Prevent closing via overlay click or dragging.

<Drawer bind:open dismissible={false} title="Non-Dismissible" description="You must use the button to close.">
  <Button label="Open Drawer" onclick={() => (open = true)} />

  {#snippet body()}
    <p>This drawer cannot be dismissed by clicking the overlay or dragging.</p>
  {/snippet}

  {#snippet footer()}
    <Button label="Close" onclick={() => (open = false)} class="w-full" />
  {/snippet}
</Drawer>

UI Slots

Use the ui prop to override classes on internal elements.

SlotDescription
overlayBackdrop overlay
contentMain drawer content
handleDrag handle
containerContainer wrapper
headerHeader section
titleTitle text
descriptionDescription text
bodyMain content area
footerFooter section

Snippets

SnippetDescription
childrenTrigger element
contentReplace entire default layout
headerCustom header
titleSlotCustom title (overrides title prop)
descriptionSlotCustom description
bodyBody content
footerFooter content

Props

PropTypeDefault
openbooleanfalse
direction'top' | 'right' | 'bottom' | 'left''bottom'
titlestring-
descriptionstring-
insetbooleanfalse
overlaybooleantrue
handlebooleantrue
dismissiblebooleantrue
portalbooleantrue
snapPoints(number | string)[]-
nestedbooleanfalse
onOpenChange(open) => void-
classstring-
uiRecord<Slot, Class>-