npm: @abhivarde/svelte-drawer

Svelte Drawer

A drawer component for Svelte 5, inspired by Vaul.

GitHub

Trusted by developers

Open source and community driven

Installation

npm install @abhivarde/svelte-drawer

Usage

Basic setup with a bottom drawer.

import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

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

<button onclick={() => open = true}>Open Drawer</button>

<Drawer bind:open>
  <DrawerOverlay />
  <DrawerContent class="bg-white rounded-t-lg p-6">
    <DrawerHandle class="mb-8" />
    <h2 class="text-lg font-medium">Drawer Title</h2>
    <p class="text-gray-600">Drawer content goes here.</p>
  </DrawerContent>
</Drawer>

Position

Change direction with the direction prop.

<Drawer direction="bottom"></Drawer>
<Drawer direction="top"></Drawer>
<Drawer direction="left"></Drawer>
<Drawer direction="right"></Drawer>

Keyboard Shortcuts

Press Esc to close the drawer. Disable with closeOnEscape=false.

<Drawer bind:open closeOnEscape=true>
  <!-- Drawer content -->
</Drawer>

Backdrop Blur

Add a premium glass-morphism effect to your drawer overlay. Choose from multiple blur intensities.

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

<Drawer bind:open>
  <DrawerOverlay blur="lg" class="fixed inset-0 bg-black/30" />
  <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-6">
    <DrawerHandle class="mb-8" />
    <h2 class="text-lg font-medium">Blurred Backdrop</h2>
    <p class="text-gray-600">
      The overlay behind this drawer has a beautiful blur effect.
    </p>
    <div class="mt-4 space-y-2">
      <p class="text-sm text-gray-500">Try different blur intensities:</p>
      <code class="text-xs">blur="sm" | "md" | "lg" | "xl" | "2xl" | "3xl"</code>
    </div>
  </DrawerContent>
</Drawer>

Prebuilt Variants

Use prebuilt variants for common drawer styles. Available variants: default, sheet, dialog, minimal, and sidebar.

<Drawer bind:open>
  <DrawerOverlay />
  <DrawerVariants variant="sheet">
    <div class="p-6">
      <h2>Sheet Variant</h2>
      <p>Prebuilt styling applied!</p>
    </div>
  </DrawerVariants>
</Drawer>

<!-- Available variants -->
<DrawerVariants variant="default" />  <!-- Default bottom drawer -->
<DrawerVariants variant="sheet" />    <!-- Full-height sheet -->
<DrawerVariants variant="dialog" />   <!-- Centered dialog -->
<DrawerVariants variant="minimal" />  <!-- Minimal bottom -->
<DrawerVariants variant="sidebar" />  <!-- Side panel -->

Snap Points

Snap points allow the drawer to rest at predefined heights, creating an iOS-like sheet experience. Drag to snap between positions or control programmatically.

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

<Drawer 
  bind:open 
  snapPoints={[0.25, 0.5, 0.9]}
  bind:activeSnapPoint
  onSnapPointChange={(point) => console.log('Snapped to:', point)}
>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">Snap Points Drawer</h2>
        <p class="text-gray-600 mb-2">
          Try dragging this drawer! It will snap to 25%, 50%, or 90% heights.
        </p>
        <p class="text-gray-600 mb-4">
          Current: {activeSnapPoint ? `${(activeSnapPoint * 100).toFixed(0)}%` : 'Loading...'}
        </p>
        <div class="space-y-2">
          <button 
            onclick={() => activeSnapPoint = 0.25}
            class="w-full px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
          >
            Snap to 25%
          </button>
          <button 
            onclick={() => activeSnapPoint = 0.5}
            class="w-full px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
          >
            Snap to 50%
          </button>
          <button 
            onclick={() => activeSnapPoint = 0.9}
            class="w-full px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
          >
            Snap to 90%
          </button>
        </div>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Portal Support

Render the drawer in a portal to avoid z-index conflicts. The portal renders the drawer at the end of the document body by default.

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

<Drawer bind:open portal={true}>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">Portal Drawer</h2>
        <p class="text-gray-600 mb-2">
          This drawer is rendered in a portal at the end of the document body.
        </p>
        <p class="text-gray-600 mb-2">
          This prevents z-index conflicts in complex layouts.
        </p>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Header & Footer Components

Optional pre-styled header and footer components for quick drawer setup. Use them for convenience or build your own custom headers/footers.

<script>
  import { 
    Drawer, 
    DrawerOverlay, 
    DrawerContent, 
    DrawerHeader, 
    DrawerFooter 
  } from '@abhivarde/svelte-drawer';

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

<Drawer bind:open>
  <DrawerOverlay />

  <DrawerContent
    class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col h-[70vh]"
  >
    <!-- Header -->
    <DrawerHeader>
      <div class="max-w-md mx-auto">
        <h2 class="text-lg font-semibold text-gray-900">
          Settings
        </h2>
        <p class="text-sm text-gray-600 mt-1">
          Manage your preferences
        </p>
      </div>
    </DrawerHeader>

    <!-- Body -->
    <div class="p-4 flex-1 overflow-y-auto">
      <div class="max-w-md mx-auto">
        <p>Your content here</p>
      </div>
    </div>

    <!-- Footer -->
    <DrawerFooter>
      <div class="max-w-md mx-auto flex justify-end gap-3">
        <button
          onclick={() => open = false}
          class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
        >
          Cancel
        </button>
        <button
          class="px-4 py-2 bg-black text-white rounded hover:bg-gray-800"
        >
          Save
        </button>
      </div>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Examples

Default Drawer

<Drawer bind:open={defaultOpen}>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] mt-24 h-fit fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">Drawer for Svelte.</h2>
        <p class="text-gray-600 mb-2">This component can be used as a Dialog replacement on mobile and tablet devices.</p>
        <p class="text-gray-600 mb-2">This is the simplest setup.</p>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Side Drawer

<Drawer bind:open={sideOpen} direction="right">
  <DrawerOverlay />
  <DrawerContent class="right-2 top-2 bottom-2 fixed outline-none w-[310px] flex">
    <div class="bg-zinc-50 h-full w-full grow p-5 flex flex-col rounded-[16px]">
      <DrawerHandle class="mb-4" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-2 text-zinc-900">It supports all directions.</h2>
        <p class="text-zinc-600 mb-2">This drawer is positioned on the right side.</p>
      </div>
      <DrawerFooter />
    </div>
  </DrawerContent>
</Drawer>

Nested Drawers

<Drawer bind:open={nested1Open}>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] h-full mt-24 lg:h-fit max-h-[96%] fixed bottom-0 left-0 right-0">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">Nested Drawers.</h2>
        <p class="text-gray-600 mb-2">Nesting drawers creates a stacking effect.</p>
        <p class="text-gray-600 mb-4">Open the second drawer to see it in action.</p>
        <button onclick={() => nested2Open = true} class="rounded-md w-full bg-gray-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-gray-800">
          Open Second Drawer
        </button>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Scrollable Drawer

<Drawer bind:open={scrollableOpen}>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] mt-24 h-[80%] lg:h-[320px] fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1 overflow-y-auto">
      <div class="max-w-md mx-auto space-y-4">
        <DrawerHandle class="mb-8" />
        <h2 class="font-medium mb-4 text-gray-900">Scrollable Drawer</h2>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Controlled Drawer

<Drawer bind:open={controlledOpen} onOpenChange={(isOpen) => console.log('Drawer is now:', isOpen ? 'open' : 'closed')}>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] mt-24 h-fit fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">A controlled drawer.</h2>
        <p class="text-gray-600 mb-2">Control the state externally while still reacting to user gestures via onOpenChange.</p>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Prebuilt Variants

<Drawer bind:open={variantOpen}>
  <DrawerOverlay />
  <DrawerVariants variant="sheet">
    <div class="p-6">
      <DrawerHandle class="mb-6" />
      <h2 class="text-xl font-semibold mb-4">Sheet Variant</h2>
      <p class="text-gray-600">This uses the prebuilt "sheet" variant style.</p>
    </div>
    <DrawerFooter />
  </DrawerVariants>
</Drawer>

Snap Points

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

<Drawer 
  bind:open 
  snapPoints={[0.25, 0.5, 0.9]}
  bind:activeSnapPoint
  onSnapPointChange={(point) => console.log('Snapped to:', point)}
>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">Snap Points Drawer</h2>
        <p class="text-gray-600 mb-2">
          Try dragging this drawer! It will snap to 25%, 50%, or 90% heights.
        </p>
        <p class="text-gray-600 mb-4">
          Current: {activeSnapPoint ? `${(activeSnapPoint * 100).toFixed(0)}%` : 'Loading...'}
        </p>
        <div class="space-y-2">
          <button 
            onclick={() => activeSnapPoint = 0.25}
            class="w-full px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
          >
            Snap to 25%
          </button>
          <button 
            onclick={() => activeSnapPoint = 0.5}
            class="w-full px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
          >
            Snap to 50%
          </button>
          <button 
            onclick={() => activeSnapPoint = 0.9}
            class="w-full px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
          >
            Snap to 90%
          </button>
        </div>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Portal Drawer

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

<Drawer bind:open portal={true}>
  <DrawerOverlay />
  <DrawerContent class="bg-gray-100 flex flex-col rounded-t-[10px] fixed bottom-0 left-0 right-0 outline-none">
    <div class="p-4 bg-white rounded-t-[10px] flex-1">
      <DrawerHandle class="mb-8" />
      <div class="max-w-md mx-auto">
        <h2 class="font-medium mb-4 text-gray-900">Portal Drawer</h2>
        <p class="text-gray-600 mb-2">
          This drawer is rendered in a portal at the end of the document body.
        </p>
        <p class="text-gray-600 mb-2">
          This prevents z-index conflicts in complex layouts.
        </p>
      </div>
    </div>
    <DrawerFooter />
  </DrawerContent>
</Drawer>

Header & Footer

<script>
  import { 
    Drawer, 
    DrawerOverlay, 
    DrawerContent, 
    DrawerHeader, 
    DrawerFooter 
  } from '@abhivarde/svelte-drawer';

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

<Drawer bind:open>
  <DrawerOverlay />

  <DrawerContent
    class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col h-[70vh]"
  >
    <!-- Header -->
    <DrawerHeader>
      <div class="max-w-md mx-auto">
        <h2 class="text-lg font-semibold text-gray-900">
          Settings
        </h2>
        <p class="text-sm text-gray-600 mt-1">
          Manage your preferences
        </p>
      </div>
    </DrawerHeader>

    <!-- Body -->
    <div class="p-4 flex-1 overflow-y-auto">
      <div class="max-w-md mx-auto">
        <p>Your content here</p>
      </div>
    </div>

    <!-- Footer -->
    <DrawerFooter>
      <div class="max-w-md mx-auto flex justify-end gap-3">
        <button
          onclick={() => open = false}
          class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
        >
          Cancel
        </button>
        <button
          class="px-4 py-2 bg-black text-white rounded hover:bg-gray-800"
        >
          Save
        </button>
      </div>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Backdrop Blur

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

<Drawer bind:open>
  <DrawerOverlay blur="lg" class="fixed inset-0 bg-black/30" />
  <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-6">
    <DrawerHandle class="mb-8" />
    <h2 class="text-lg font-medium">Blurred Backdrop</h2>
    <p class="text-gray-600">
      The overlay behind this drawer has a beautiful blur effect.
    </p>
    <div class="mt-4 space-y-2">
      <p class="text-sm text-gray-500">Try different blur intensities:</p>
      <code class="text-xs">blur="sm" | "md" | "lg" | "xl" | "2xl" | "3xl"</code>
    </div>
  </DrawerContent>
</Drawer>

Persistent State

<script>
  import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

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

<Drawer 
  bind:open 
  persistState={true}
  persistKey="settings-drawer"
>
  <DrawerOverlay />
  <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-6">
    <DrawerHandle class="mb-8" />
    <h2 class="text-lg font-medium">Persistent Drawer</h2>
    <p class="text-gray-600">
      This drawer remembers if it was open. Try opening it, then reload the page!
    </p>
    <p class="text-gray-600 mt-4">
      Close it and reload - it will stay closed.
    </p>
  </DrawerContent>
</Drawer>

Persistent with Snap Points

<script>
  import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

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

<Drawer 
  bind:open 
  snapPoints={[0.25, 0.5, 0.9]}
  bind:activeSnapPoint={snapPoint}
  persistState={true}
  persistKey="snap-settings"
  persistSnapPoint={true}
>
  <DrawerOverlay />
  <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-6">
    <DrawerHandle class="mb-8" />
    <h2 class="text-lg font-medium">Persistent Snap Points</h2>
    <p class="text-gray-600">
      Drag to different heights, then reload the page. The position is saved!
    </p>
    <p class="text-sm text-gray-500 mt-4">
      Current: {snapPoint ? `${(snapPoint * 100).toFixed(0)}%` : 'Default'}
    </p>
  </DrawerContent>
</Drawer>