@drawer-kit/svelte
v0.1.0
Published
A headless, primitive drawer/sidebar component for Svelte 5 — inline or overlay, collapsible, resizable, RTL-aware, zero runtime icon dependency.
Maintainers
Readme
@drawer-kit/svelte
A headless-friendly, primitive drawer/sidebar component for Svelte 5
(runes). Inline or overlay, collapsible to an icon rail, resizable,
RTL-aware, SSR-safe, and stateful via a single DrawerController class
you own and can share across components.
No runtime icon-library dependency — every icon slot in the package is a plain Svelte snippet, with a small dependency-free SVG as the fallback when you don't supply one.
Install
npm install @drawer-kit/svelteRequires svelte ^5.20.0 as a peer dependency (needed for the
$props.id() rune the popover uses internally for SSR-safe unique ids).
Quick start
<script lang="ts">
import { Drawer, DrawerLayout, DrawerController } from '@drawer-kit/svelte';
const nav = new DrawerController({
variant: 'auto', // inline on desktop, overlay below the mobile breakpoint
collapsible: true, // adds a third 'collapsed' (icon-rail) mode
initialMode: 'open'
});
</script>
<DrawerLayout controller={nav}>
{#snippet panel()}
<Drawer controller={nav} headerHeight={56}>
{#snippet children(ctrl)}
<!-- your nav content -->
{/snippet}
</Drawer>
{/snippet}
{#snippet children()}
<!-- your page content -->
{/snippet}
</DrawerLayout>DrawerController is a plain class — construct it once (in module scope,
a $state-holding parent component, or wherever fits your app) and pass
the same instance to every element that needs to read or drive it.
Core concepts
- Modes:
'closed' | 'collapsed' | 'open'.'collapsed'requirescollapsible: trueand is inline-only — it's automatically cleared when the effective variant promotes to overlay. - Variant:
'inline' | 'overlay' | 'auto'(default'auto': inline above the mobile breakpoint, overlay below it).effectiveVarianton the controller is always the resolved, renderable value. - Resizable:
resizable: true+minWidth/maxWidthadds a drag-to-resize handle on the panel's open edge. - Scoped mounting: pass
mountTargetto scope an overlay drawer (and its backdrop/edge button) to a specific container instead of the viewport. - RTL: every logical-direction detail (which physical edge a
placement="start"panel renders on, which way it slides, which side its border sits on) is derived automatically — seeplacementandresolveOverlayTranslate/resolvePanelPositionClassesif you're building custom pieces on top of the primitives.
Components
| Component | Purpose |
| --- | --- |
| DrawerController | Owns all state and transition logic. Framework-agnostic aside from runes. |
| DrawerLayout | Row wrapper: panel + content pane, with a scrollStrategy choice. |
| Drawer | The panel itself: header, edge button, resize handle, scroll region. |
| DrawerPanel / DrawerContent / DrawerBackdrop | Lower-level pieces Drawer/DrawerLayout compose — exported for custom layouts. |
| DrawerEdgeButton | The open/collapse/close toggle rendered on the panel's edge. |
| DrawerGroup | A nav section header/divider with an optional expand/collapse chevron. |
| DrawerPopover | An anchored popover, positioned relative to any element (e.g. a collapsed-rail item). |
| PrefixTile / ContentTile / NavigationSkeleton | Small nav-row building blocks (icon slot, animated label, loading placeholder). |
Every prop type, plus every pure decision/style-building function the
components are built from (resolveToggleTarget, resolvePanelWidth,
etc.) and every tunable constant (DEFAULT_OPEN_WIDTH,
DRAWER_TRANSITION_MS, ...) is exported from the package root, so you can
build a related custom piece without duplicating this package's logic.
Icon injection
Icon slots (the edge button's open/collapse/close icons, DrawerGroup's
chevron, PrefixTile's leading icon) are plain Svelte snippets — the
library has no icon-library dependency of its own:
<script lang="ts">
import { Drawer } from '@drawer-kit/svelte';
import { PanelLeftOpen, PanelLeftClose, X } from 'lucide-svelte';
</script>
<Drawer
controller={nav}
edgeButton={{
icons: { open: openIcon, collapse: collapseIcon, close: closeIcon }
}}
>
{#snippet openIcon({ size })}<PanelLeftOpen {size} />{/snippet}
{#snippet collapseIcon({ size })}<PanelLeftClose {size} />{/snippet}
{#snippet closeIcon({ size })}<X {size} />{/snippet}
{#snippet children(ctrl)}...{/snippet}
</Drawer>Omit any of these and the package falls back to its own tiny
dependency-free inline SVG — same position, same behavior, no bundle-size
cost for consumers who don't need an icon library. Because these are
ordinary snippets, they compose like any other Svelte value: a snippet
can itself render another snippet passed into it (see the "Icon
injection" demo in examples/svelte for a badge-wrapped icon built this
way) — the library places no restriction on nesting.
Native scroll restoration vs. resetScroll()
DrawerLayout's content pane is its own scroll container by default
(scrollStrategy="container"), which is invisible to your router's
window-level scroll restoration. Prefer scrollStrategy="document" when
your app's root layout can let the document itself scroll — restoration
then works natively, for free.
When it can't, call the exported resetScroll() method from your own
router's "navigation happened" hook — the package deliberately has no
router dependency of its own:
<script lang="ts">
import { afterNavigate } from '$app/navigation'; // SvelteKit example
let layout: ReturnType<typeof DrawerLayout> | undefined;
afterNavigate(() => layout?.resetScroll());
</script>
<DrawerLayout controller={nav} bind:this={layout}>...</DrawerLayout>Breaking change (unreleased): the
resetScrollOnNavigateprop (a SvelteKit-only,$app/navigation-coupled implementation of this) has been removed in favor of the router-agnosticresetScroll()method above — the old prop made the package fail to build for any non-SvelteKit consumer.
Accessibility
- The edge button and backdrop both compute their
aria-label/action from the controller's current mode — override vialabels. DrawerPopoveruses$props.id()for itsaria-describedby/aria-controlswiring, so ids stay stable across SSR and hydration.- All logical-direction/RTL handling described above applies to focus order and swipe/slide direction too, not just layout.
Example
A runnable demo lives in examples/svelte — see
that directory's README for how to run it.
Full API reference
src/docs/Drawer.md in this repository has the complete prop-by-prop
reference for every component (not included in the published npm
package — clone the repo or browse it on GitHub).
