Issue Nº 01 · Apr 2026 · MIT Filed under: gestures · physics · a11y

@surdeddd/ bottom-sheet

A universal sheet engine. One framework-agnostic core, seven adapters, native-feel gestures.

21KB gzip core / esm
7 adapters
4 modes
500+200 tests · unit + e2e
0 runtime deps
§ 01 — the anatomy

Five layers, one gesture

Every layer below is a real node the engine owns — nothing here is a picture of a sheet.

↓ scroll to build it

  1. .bs-backdrop 01 Backdrop opacity tracks progress
  2. .bs-screen 02 Scrim above-sheet · blur · presets
  3. .bs-sheet 03 Surface translate3d only · 0 reflow
  4. .bs-handle 04 Handle role=slider · drag zones
  5. .bs-content 05 Content scrollable · drag-from-content
§ 02 — the device
360×720 · bottom mode ↑↓ Esc Tab
§ 02 — playground

Snap point

Mode

Spring physics (remounts)

Behavior

Drag surface

Pull the sheet by its handle only.

Scrim

§ 03 — advanced

Snap-point editor

Add, remove, edit, reorder snaps. Sheet remounts live.

    Stress test

    Cycle min↔full at decreasing intervals. Watch the FPS.

    idle

    Floating UI

    Anchored elements ride the sheet edge on the compositor; scrim stages swap content per state.

    Theme preset

    Opt-in via class on .bs-root. Loads the theme CSS lazily.

    default

    § 05 — what's inside

    What's inside — sixteen

    F.01

    Spring physics

    Critical-damped harmonic oscillator. Drag velocity carries into the settle — no jerk on flick. Sub-stepped at 240Hz, one DOM write per RAF.

    stiffness · damping · mass
    F.02

    GPU-only motion

    Sized once via height; resized via transform: translate3d. Zero layout per frame. will-change gated to drag/animate.

    60fps drag · 0 reflow
    F.03

    Any CSS length

    Snap points accept px, percentages, fit, full, plus arbitrary CSS: 50dvh, clamp(), env().

    probe-element resolver
    F.04

    Four directions

    bottom · top · left · right. Same engine — drawer-side sheets without code duplication. Pointer axis & sign computed.

    switch (mode) { ... }
    F.05

    Pointer-type tuned

    Touch flicks: 0.65px/ms threshold, 120ms velocity window. Mouse inertia: 0.4px/ms, 160ms window. Gesture parses what the pointer is.

    PointerEvent.pointerType
    F.06

    Native a11y

    role=slider on handle. ↑↓ steps snaps, Home/End jumps, Esc closes. Focus trap with restore. ARIA-live announcer. WCAG 2.1 AA.

    keyboard · screen-reader · inert
    F.07

    Mobile primitives

    env(safe-area-inset-*), hardware-back interception, haptic tick on snap, body scroll lock that's iOS-safe (position:fixed).

    iPhone · Android Chrome
    F.08

    Headless or framed

    Use useBottomSheet() for full JSX control, or grab the ready-made <BottomSheet>. Three layers, your call.

    React · Vue · Svelte · Solid · Lit · WC · core
    F.09

    Multi-sheet stack

    Open a sheet from another sheet. Z-index orchestrated, backdrops de-duped, only the topmost owns Escape.

    sheetStack singleton
    F.10

    SSR-safe

    Zero window at import. useSyncExternalStore with cached snapshot. Optional noSSR prop kills hydration mismatches in Next.js.

    React 18+ · Vue 3+
    F.11

    Sheet manager

    createSheetManager() — typed registry mapping route keys to configs. onOpen/onClose hooks like the original Vue 2 manager, framework-agnostic.

    manager.transition(prev, next)
    F.12

    Tested

    500+ unit tests (vitest + happy-dom) covering snap math, spring, gestures, focus trap, scroll lock, manager, vh→dvh, viewport resize. ~200 e2e via Playwright on Chromium, WebKit and Firefox. All green.

    npm test · npx playwright test
    F.13

    Drag zones

    dragFrom: handle, whole sheet, or opt-in [data-bs-drag] islands. [data-bs-no-drag] excludes a subtree, so carousels keep their own gesture.

    setDragFrom · runtime switch
    F.14

    Anchored UI

    Elements ride the sheet edge on the compositor via --bs-size. Scrim stages swap content per state; docked bars let the sheet slide under them.

    anchors · scrim stages
    F.15

    Pay for what you use

    /core is the engine alone at ~18 KB. Route, persist, auto-collapse and the rest are opt-in EngineFeatures you register per instance.

    core 18 KB · features 2.4 KB
    F.16

    Debuggable

    attachDebugOverlay(engine) prints live snap, size, progress and flags from public events only — and ships zero bytes into every other bundle.

    /debug · 937 B