Getting started
This guide takes you from installation to a visible, interactive drawer. It uses the package’s primary module entrypoint; for a classic <script> setup, see Browser / CDN.
Requirements
Section titled “Requirements”- Node.js 20 or newer for installation and builds.
- A modern browser with ES2020, Pointer Events, and standard dialog-related DOM APIs.
- Your own panel geometry and theme. The package stylesheet provides motion and interaction behavior, not a finished visual design.
1. Install the package
Section titled “1. Install the package”npm install @samline/drawerpnpm add @samline/draweryarn add @samline/drawerbun add @samline/drawer2. Add a trigger
Section titled “2. Add a trigger”<button id="open-filters" type="button">Open filters</button>3. Import the runtime and stylesheet
Section titled “3. Import the runtime and stylesheet”import { createDrawer } from '@samline/drawer'import '@samline/drawer/styles.css'
const trigger = document.querySelector<HTMLElement>('#open-filters')
const drawer = createDrawer({ id: 'filters', triggerElement: trigger, direction: 'bottom', title: 'Filters', description: 'Refine the result set', content: 'Add your filter controls here.', showHandle: true, closeButton: true})
// Release the host and listeners when this page or widget unmounts.window.addEventListener('pagehide', () => drawer.destroy(), { once: true })4. Position and theme the panel
Section titled “4. Position and theme the panel”[data-drawer-overlay] { position: fixed; inset: 0; z-index: 40; background: rgb(15 23 42 / 55%);}
[data-drawer] { position: fixed; z-index: 41; box-sizing: border-box; width: min(100%, 42rem); max-height: 85dvh; padding: 1.25rem; overflow: auto; color: #172033; background: #fff; outline: none;}
[data-drawer-direction='bottom'] { right: 0; bottom: 0; left: 0; margin-inline: auto; border-radius: 1.25rem 1.25rem 0 0;}Click Open filters. The external trigger opens the drawer; the overlay, Escape key, drag gesture, or built-in close button closes it.
The controller
Section titled “The controller”createDrawer() returns a controller for the normalized id. Keep it when your integration needs to read, update, subscribe to, or destroy the drawer.
const drawer = createDrawer({ id: 'filters', title: 'Filters' })
drawer.setOpen(true)drawer.setActiveSnapPoint('420px')drawer.update({ content: 'Updated body' })
const unsubscribe = drawer.subscribe((snapshot) => { console.log(snapshot.state.isOpen)})
unsubscribe()drawer.destroy()Important distinctions:
setOpen(),setActiveSnapPoint(), andpatch()return a state snapshot.patch()accepts onlyCommonDrawerOptions; it cannot updatetitle,content, triggers, or classes.update()accepts allVanillaDrawerOptionsand returns another controller wrapper for the same id.- Subscribers run immediately and on every controller publication, including some same-value writes.
- Treat
drawer.optionsand snapshot objects as read-only. They expose live nested references; mutate state through controller methods so rendering and subscribers stay synchronized.
See Controller and registry for every property, method, return value, and the equivalent id-based helpers.
Content forms
Section titled “Content forms”title, description, and content accept text, numbers, existing elements, or lazy element factories.
const form = document.createElement('form')form.innerHTML = '<label>Search <input name="q" /></label><button>Apply</button>'
createDrawer({ id: 'search', title: 'Search', content: form // moved into the drawer, not cloned})
createDrawer({ id: 'clock', title: 'Current time', content: () => { const time = document.createElement('time') time.textContent = new Date().toLocaleTimeString() return time }})Lazy factories run again whenever the dialog subtree is rebuilt, including after close and reopen. Existing elements become detached when that subtree is removed; retain your own reference if you need to reattach or reuse one. Read the exact VanillaRenderable contract.
Lifecycle
Section titled “Lifecycle”createDrawer()registers one host per id. A closed drawer keeps only that host and its optional built-in trigger.- Opening mounts the dialog and, when
overlayresolves totrue, a backdrop.overlaydefaults tomodal. Creating withopen: trueskips the entrance animation; create closed and then callsetOpen(true)to animate. - Closing marks visual nodes closed, releases focus and page effects, then removes those nodes after the exit timeout. It keeps the id and host registered.
- Destroying removes the registry entry, host, listeners, timers, and effects owned by that drawer. It does not call
onClose().
Reusing an id shallow-merges options into the existing drawer. Use a unique id for an independent drawer and call destroy() when its integration unmounts.
Choose the next page
Section titled “Choose the next page”- Lifecycle and state explains identity, close versus destroy, callback order, subscriptions, and SPA cleanup.
- Gestures and snap points explains drag permission, snap ordering, nesting, and scaled backgrounds.
- Accessibility and focus covers naming, modal focus ownership, dismissal, and interactive content.
- Configuration documents every option, default, accepted value, and important edge case.
- Controller and registry helps choose between instance methods, id-based helpers, and the headless controller.
- Entrypoints compares ESM, CommonJS, the browser namespace, and the standalone IIFE.
- Styling defines the DOM, ARIA, selectors, inline styles, and all four directions.
- Recipes covers snap points, nesting, forms, focus, triggers, multiple drawers, and cleanup.
- TypeScript lists all exported types, constants, and return shapes.