A drawer component for Svelte 5, inspired by Vaul.
npm install @abhivarde/svelte-drawer
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<button onclick={() => open = true}>
Open Drawer
</button>
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<DrawerHandle class="mb-8" />
<h2>Drawer Content</h2>
<p>This is a drawer component.</p>
<button onclick={() => open = false}>Close</button>
</DrawerContent>
</Drawer>
Add a premium blur effect to the overlay background:
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<Drawer bind:open>
<!-- Default medium blur -->
<DrawerOverlay blur class="fixed inset-0 bg-black/40" />
<!-- Or specify blur intensity -->
<!-- <DrawerOverlay blur="sm" class="fixed inset-0 bg-black/40" /> -->
<!-- <DrawerOverlay blur="lg" class="fixed inset-0 bg-black/40" /> -->
<!-- <DrawerOverlay blur="xl" class="fixed inset-0 bg-black/40" /> -->
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<DrawerHandle class="mb-8" />
<h2>Blurred Backdrop</h2>
<p>Notice the premium blur effect behind this drawer.</p>
</DrawerContent>
</Drawer>
Available blur intensities:
blur={true} or blur="md" - Medium blur (default)blur="sm" - Small blurblur="lg" - Large blurblur="xl" - Extra large blurblur="2xl" - 2x extra large blurblur="3xl" - 3x extra large blur<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<Drawer bind:open direction="right">
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed right-0 top-0 bottom-0 w-80 bg-white p-4">
<DrawerHandle class="mb-4" />
<h2>Side Drawer</h2>
<button onclick={() => open = false}>Close</button>
</DrawerContent>
</Drawer>
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
function handleOpenChange(isOpen) {
console.log('Drawer is now:', isOpen ? 'open' : 'closed');
open = isOpen;
}
</script>
<Drawer bind:open onOpenChange={handleOpenChange}>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<DrawerHandle class="mb-8" />
<h2>Controlled Drawer</h2>
</DrawerContent>
</Drawer>
<script>
import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<!-- Disable Escape key -->
<Drawer bind:open closeOnEscape={false}>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<h2>Cannot close with Escape</h2>
</DrawerContent>
</Drawer>
<!-- Disable focus trap -->
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent trapFocus={false} class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<h2>Tab navigation not restricted</h2>
</DrawerContent>
</Drawer>
<script>
import { Drawer, DrawerOverlay, DrawerVariants, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<!-- Sheet variant (iOS-style bottom sheet) -->
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerVariants variant="sheet">
<div class="p-6">
<DrawerHandle class="mb-6" />
<h2>iOS-style Sheet</h2>
<p>Clean and modern bottom sheet design</p>
</div>
</DrawerVariants>
</Drawer>
<!-- Dialog variant (center modal) -->
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerVariants variant="dialog">
<div class="p-6">
<h2>Dialog Style</h2>
<p>Center-positioned modal dialog</p>
</div>
</DrawerVariants>
</Drawer>
<!-- Sidebar variant (navigation drawer) -->
<Drawer bind:open direction="right">
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerVariants variant="sidebar">
<div class="p-6">
<DrawerHandle class="mb-4" />
<h2>Sidebar Navigation</h2>
<p>Side navigation drawer</p>
</div>
</DrawerVariants>
</Drawer>
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<!-- Default gray handle -->
<DrawerHandle class="mb-8" />
<!-- Custom colored handle -->
<!-- <DrawerHandle class="bg-blue-500 mb-8" /> -->
<!-- Larger handle -->
<!-- <DrawerHandle class="w-16 h-2 mb-8" /> -->
<h2>Drawer with Custom Handle</h2>
<p>The handle automatically adapts to drawer direction.</p>
</DrawerContent>
</Drawer>
Snap points allow the drawer to rest at predefined heights, creating an iOS-like sheet experience.
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
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 class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<DrawerHandle class="mb-8" />
<h2>Drawer with Snap Points</h2>
<p>Drag to see snapping behavior at 25%, 50%, and 90%</p>
<!-- Programmatically change snap point -->
<button onclick={() => activeSnapPoint = 0.5}>Jump to 50%</button>
</DrawerContent>
</Drawer>
How it works:
0.5 = 50% of screen height)bind:activeSnapPoint to programmatically control the current positiononSnapPointChange callback to react to snap changesRender the drawer in a portal to avoid z-index conflicts in complex layouts.
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<!-- Enable portal (renders at end of body) -->
<Drawer bind:open portal={true}>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
<DrawerHandle class="mb-8" />
<h2>Portal Drawer</h2>
<p>This drawer is rendered in a portal, preventing z-index issues.</p>
</DrawerContent>
</Drawer>
<!-- Custom portal container -->
<Drawer bind:open portal={true} portalContainer="#custom-portal">
<DrawerOverlay />
<DrawerContent>
<h2>Custom Portal</h2>
</DrawerContent>
</Drawer>
<div id="custom-portal"></div>
When to use portals:
Optional pre-styled header and footer components for quick setup.
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerHeader, DrawerHandle } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<!-- With title and description -->
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg">
<DrawerHeader
title="Drawer Title"
description="Optional description text"
showCloseButton={true}
/>
<div class="p-4">
<p>Drawer content here</p>
</div>
</DrawerContent>
</Drawer>
<!-- Custom header content -->
<Drawer bind:open>
<DrawerOverlay />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg">
<DrawerHeader>
<div class="flex items-center gap-3">
<img src="/icon.png" alt="Icon" class="w-8 h-8" />
<div>
<h2 class="font-semibold">Custom Header</h2>
<p class="text-sm text-gray-600">Your custom content</p>
</div>
</div>
</DrawerHeader>
<div class="p-4">
<p>Drawer content</p>
</div>
</DrawerContent>
</Drawer>
<script>
import { Drawer, DrawerOverlay, DrawerContent, DrawerFooter } from '@abhivarde/svelte-drawer';
let open = $state(false);
</script>
<!-- Simple footer with custom content -->
<Drawer bind:open>
<DrawerOverlay class="fixed inset-0 bg-black/40" />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col">
<div class="p-4 flex-1">
<h2>Drawer Content</h2>
</div>
<DrawerFooter>
<button onclick={() => open = false} class="px-4 py-2 bg-gray-200 rounded">
Cancel
</button>
<button class="px-4 py-2 bg-black text-white rounded">
Confirm
</button>
</DrawerFooter>
</DrawerContent>
</Drawer>
<!-- Custom footer with links -->
<Drawer bind:open>
<DrawerOverlay />
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg flex flex-col">
<div class="p-4 flex-1">
<h2>Content</h2>
</div>
<DrawerFooter class="bg-gray-50">
<div class="flex justify-between w-full">
<a href="/privacy" class="text-sm text-gray-600 hover:text-gray-900">Privacy</a>
<a href="/terms" class="text-sm text-gray-600 hover:text-gray-900">Terms</a>
</div>
</DrawerFooter>
</DrawerContent>
</Drawer>
Note: These components are optional. You can still build custom headers and footers using plain HTML/Svelte markup without importing these components.
Available variants for DrawerVariants component:
default - Standard bottom drawer with gray backgroundsheet - iOS-style bottom sheet (white, rounded, 85vh height)dialog - Center modal dialog styleminimal - Simple bottom drawer without extra stylingsidebar - Side navigation drawer (full height)closeOnEscape={false})Main wrapper component that manages drawer state and animations.
Props:
open (boolean, bindable) - Controls the open/closed stateonOpenChange (function, optional) - Callback when open state changesdirection ('bottom' | 'top' | 'left' | 'right', default: 'bottom') - Direction from which drawer slidescloseOnEscape (boolean, optional, default: true) - Whether Escape key closes the drawersnapPoints (number[], optional) - Array of snap positions between 0-1activeSnapPoint (number, bindable, optional) - Current active snap point valueonSnapPointChange (function, optional) - Callback fired when snap changesportal (boolean, optional, default: false) - Render drawer in a portalportalContainer (HTMLElement | string, optional) - Custom portal container element or selectorOverlay component that appears behind the drawer.
Props:
class (string, optional) - CSS classes for stylingblur (boolean | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl', optional) - Enable backdrop blur effectContent container for the drawer.
Props:
class (string, optional) - CSS classes for stylingtrapFocus (boolean, optional, default: true) - Whether to trap focus inside drawerVisual drag indicator that automatically adapts to drawer direction.
Props:
class (string, optional) - CSS classes for stylingFeatures:
bottom/top drawers (12px wide, 1.5px tall)left/right drawers (1.5px wide, 12px tall)data-drawer-drag attribute for improved touch targetingExample:
<!-- Default gray handle -->
<DrawerHandle class="mb-8" />
<!-- Custom color -->
<DrawerHandle class="bg-blue-500 mb-8" />
<!-- Larger size -->
<DrawerHandle class="w-16 h-2 mb-8" />
Pre-styled drawer content with built-in variants.
Props:
variant ('default' | 'sheet' | 'dialog' | 'minimal' | 'sidebar', default: 'default') - Preset style variantclass (string, optional) - Additional CSS classes for stylingtrapFocus (boolean, optional, default: true) - Whether to trap focus inside drawerOptional pre-styled header component.
Props:
title (string, optional) - Header title textdescription (string, optional) - Description text below titleshowCloseButton (boolean, optional, default: true) - Show close buttononClose (function, optional) - Custom close handlerclass (string, optional) - CSS classes for stylingFeatures:
Optional pre-styled footer component.
Props:
class (string, optional) - CSS classes for stylingFeatures:
Visit drawer.abhivarde.in to see live examples.
This project is licensed under the MIT License.
See the LICENSE file for details.
Inspired by Vaul by Emil Kowalski.