@bottom-sheet-kit/svelte
v0.1.1
Published
A primitive, headless-friendly bottom sheet component for Svelte 5 — DaisyUI styled, RTL-native, fully overridable via snippets.
Maintainers
Readme
@bottom-sheet-kit/svelte
A primitive, headless-friendly bottom sheet for Svelte 5 (runes), SSR-safe.
Styled with DaisyUI + Tailwind CSS v4, native RTL support, and every
part overridable via snippets. dist/index.js bundles the pure logic
(constants, utils, types) into one minified file, plus one rolled-up
dist/index.d.ts; .svelte component files and the SheetController reactive
module ship as source (the latter is a .svelte.js-suffixed file, Svelte 5's
mechanism for a package to share reactive state — it's compiled by your own
Vite/SvelteKit setup, same as the components) so SSR keeps working correctly.
- Flutter-inspired — free-resize drag, named detents (
peek,half,full, …), velocity-aware snap/close - Composable — use the pre-wired
<BottomSheet>shell, or the raw primitives (SheetPanel,SheetHandle,SheetBackdrop,SheetContent) plus an injectableSheetControllerto build a fully custom sheet - Fully overridable — replace the handle or backdrop entirely via snippets; the content area is just a slot
- Accessible —
role="dialog",aria-modal,aria-expanded, keyboard toggle (Enter/Space) on the handle, focus-visible ring - RTL-native — uses logical (
inset-inline) Tailwind utilities; flips automatically underdir="rtl" - Dark-mode-native — uses DaisyUI semantic color tokens (
base-100,base-content,base-300), so it follows whatever DaisyUI theme is active - Responsive — full-width sheet on small screens, capped and centered on
sm:and above - SSR-safe — every DOM/
window/documentaccess is guarded; portal + controller no-op during server render
Install
npm install @bottom-sheet-kit/sveltesvelte@^5 is a peer dependency. Tailwind CSS v4 + DaisyUI v5 must be
configured in the host app, since this package ships only class names, not CSS.
Quick start
<script lang="ts">
import { BottomSheet } from '@bottom-sheet-kit/svelte';
let open = $state(false);
</script>
<button onclick={() => (open = true)}>Open</button>
{#if open}
<BottomSheet startOpen initialPercent={50} onClose={() => (open = false)}>
{#snippet children(ctrl)}
<div class="p-5">
<button onclick={() => ctrl.close()}>Close</button>
</div>
{/snippet}
</BottomSheet>
{/if}<BottomSheet> props
| Prop | Type | Default | Notes |
| --------------------- | ---------------------------------- | --------- | ------------------------------------------------------------------ |
| controller | SheetController | — | Provide your own to control the sheet from a sibling button. |
| initialPercent | number | 50 | Forwarded to the internal controller when none is given. |
| minPercent | number | 15 | Below this, releasing the drag auto-closes. |
| maxPercent | number | 100 | Sheet cannot be dragged above this. |
| snapPoints | number[] | — | Omit for free-resize mode (sheet stays wherever released). |
| closeVelocity | number | 500 | px/s flick speed that force-closes regardless of position. |
| draggable | boolean | true | Disable for programmatic-only control. |
| closeOnOutsideClick | boolean | true | Backdrop tap closes the sheet. |
| showBackdrop | boolean | true | |
| startOpen | boolean | false | |
| onOpen / onClose | () => void | — | |
| onPositionChange | (percent: number) => void | — | Fires continuously during drag/animation. |
| handleVariant | 'bar' \| 'dots' \| 'none' | 'bar' | |
| handle / backdrop | Snippet<[SheetController]> | — | Full override of the built-in handle/backdrop. |
| class / sheetClass| string | — | Extra classes on the portal root / panel respectively. |
| hidden | boolean | false | Keeps the sheet mounted (so its ResizeObserver stays alive) but visually hidden — e.g. desktop mode. |
Named detents
import { sheetDetent } from '@bottom-sheet-kit/svelte';
// hidden=0, peek=10, quarter=25, half=50, threeQuarters=75, full=100
<BottomSheet snapPoints={[sheetDetent.peek, sheetDetent.half, sheetDetent.full]}>Raw primitives (fully custom UI)
<script lang="ts">
import { SheetController, SheetPanel, SheetHandle, SheetContent } from '@bottom-sheet-kit/svelte';
const ctrl = new SheetController({ snapPoints: [25, 50, 100] });
let containerEl = $state<HTMLElement>();
</script>
<div bind:this={containerEl}>
{#if containerEl}
<SheetPanel controller={ctrl} {containerEl}>
<SheetHandle controller={ctrl} variant="dots" />
<SheetContent controller={ctrl}>Custom content…</SheetContent>
</SheetPanel>
{/if}
</div>SheetController
The state/behaviour brain — a plain class using $state runes, with no DOM or
template of its own. Create it outside any component to share it with sibling
trigger buttons, headers, etc.
Reactive state: currentPercent, state (closed│open│dragging│animating), backdropOpacity, isDragging, isAnimating, isOpen.
Methods: open(percent?), close(), snapTo(percent).
Snap & release algorithm
On pointer release, in priority order:
- Velocity close — a downward flick faster than
closeVelocitycloses unconditionally. - Position close — below
minPercent(and not rescued by an upward flick) closes. - Snap to nearest (only when
snapPointsis explicit) — with velocity bias toward the next point in the drag direction. Without explicitsnapPoints, the sheet stays wherever it was released ("free-resize" mode).
Scroll-lock coordination
SheetContent prevents inner scrolling from fighting the drag-to-resize gesture:
at scroll-top a downward drag resizes the sheet; once scrolled, a downward drag
scrolls the content instead. Scroll position resets to top whenever the sheet closes.
Low-level utils
The pure math (percentToTranslateY, resolveReleaseTarget, computeVelocity,
normaliseSnapPoints, …) is exported from the package root for custom
integrations or testing — see src/utils/sheet.ts.
License
MIT
