A drawer component for Svelte 5, inspired by Vaul.
UI library: syncui.design
by abhivarde.in
Open source and community driven
npm install @abhivarde/svelte-drawer 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> Change direction with the direction prop.
<Drawer direction="bottom"></Drawer>
<Drawer direction="top"></Drawer>
<Drawer direction="left"></Drawer>
<Drawer direction="right"></Drawer> Press Esc to close the drawer. Disable with closeOnEscape=false.
<Drawer bind:open closeOnEscape=true>
<!-- Drawer content -->
</Drawer> 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> 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 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> 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> 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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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> <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>