npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@samline/drawer

v4.2.1

Published

Framework-agnostic drawer runtime with a vanilla/browser API and an IIFE bundle for direct CDN use.

Downloads

900

Readme

Drawer

A small, framework-free drawer runtime for vanilla JS and direct browser usage.

It binds to a host element, keeps drawer state in sync with the DOM, runs the drag / snap / scale pipeline, and ships a typed controller for the consumer.


Table of Contents


Installation

npm install @samline/drawer
pnpm add @samline/drawer
bun add @samline/drawer
yarn add @samline/drawer

Requires Node 20+ when bundling. Runtime target is ES2020.


CDN / Browser

Use the browser build when you do not have a bundler and need to run the package directly in HTML, Shopify, WordPress, or any traditional template.

<link rel="stylesheet" href="https://unpkg.com/@samline/[email protected]/dist/style.css" />
<script src="https://unpkg.com/@samline/[email protected]/dist/browser/global.global.js"></script>

Pin the version in production. Replace 4.2.1 with the version you ship.

The browser bundle exposes a single global: window.Drawer.

Bundler consumers can import the exact same singleton as browser; server-rendered integrations may use its newDrawer({ id, html, options }) helper and read-only available compatibility view.

<form id="contact-form">
  <button id="open-drawer" type="button">Open</button>
</form>

<link rel="stylesheet" href="https://unpkg.com/@samline/[email protected]/dist/style.css" />
<script src="https://unpkg.com/@samline/[email protected]/dist/browser/global.global.js"></script>
<script>
  window.Drawer.createDrawer({
    id: 'demo',
    triggerElement: document.getElementById('open-drawer'),
    direction: 'bottom',
    title: 'Demo',
    content: 'Hello from the browser'
  })
</script>

The methods on window.Drawer share a module-level registry keyed by id. Controllers are not stored as properties on the namespace; inspect them with window.Drawer.getDrawer(id) or window.Drawer.getDrawers(). window.Drawer.destroyDrawer(id) tears down the matching instance and removes it from that registry.

See docs/browser.md for the full browser surface.


Entrypoints

| Entrypoint | When to use | | ------------------------- | ---------------------------------------------------------------------- | | @samline/drawer | Main vanilla API for bundlers, ESM, or CJS consumers. | | @samline/drawer/browser | ESM/CJS browser namespace with named and default exports for bundlers. |

The root entrypoint exports the individual helpers. Bundled applications that prefer a namespace can import Drawer from @samline/drawer/browser. Plain <script> usage loads the dedicated IIFE at dist/browser/global.global.js, as shown above.


Quick Start

import { createDrawer } from '@samline/drawer'
import '@samline/drawer/styles.css'

const drawer = createDrawer({
  id: 'filters',
  direction: 'bottom',
  title: 'Filters',
  content: 'Filter body',
  showHandle: true,
  snapPoints: ['180px', '420px', 1],
  activeSnapPoint: '180px'
})

drawer.setOpen(true)

What this does:

  • Binds a controller to the id filters. Reusing the id is an update, not a second mount.
  • Mounts the dialog in the bottom direction with the open transition.
  • Renders the built-in handle. Clicking the handle advances the active snap point.
  • Positions the content at the 180px snap on open; the user can drag to 420px or the zero-offset snap represented by numeric 1.
  • Locks body scroll while the drawer is open (because modal defaults to true).

What You Can Build

  • Mobile-style bottom sheets, side panels, and modal dialogs with a typed controller.
  • Nested drawer flows (parent → child) where the parent scales and shifts when the child opens.
  • Snap-point flows (Spotify-like mini-player, sheet with a handle, drawer with a "show more" anchor).
  • Scale-background flows that dim and shift the page shell while the drawer is dragged open.
  • Drawers with a built-in handle, built-in trigger button, or built-in close button — no manual document.addEventListener boilerplate.
  • Browser / CDN / Shopify / WordPress embeds via the window.Drawer IIFE bundle.
  • HMR-safe SPAs under Vite using stable ids and explicit teardown.
  • Drawers that honour the mobile keyboard through the default-enabled, focus-gated repositionInputs pipeline (fixed is optional).
  • Drawers that opt out of scroll restoration (preventScrollRestoration: true).
  • Test-friendly flows via the headless createDrawerController API (no DOM, no side effects).

Custom Content

The content slot (and the title / description slots) accept the same VanillaRenderable shape: string | number | HTMLElement | (() => HTMLElement) | null | undefined. Pick the form that matches how you build your UI.

import { createDrawer } from '@samline/drawer'

// 1. Plain text.
createDrawer({ id: 'a', content: 'Hello' })

// 2. Pre-built element (the runtime moves it into the body slot).
const form = document.createElement('form')
form.innerHTML = '<input name="q" /><button>Search</button>'
createDrawer({ id: 'b', title: 'Search', content: form })

// 3. Lazy thunk (re-invoked every time the dialog subtree is rebuilt).
createDrawer({
  id: 'c',
  content: () => {
    const node = document.createElement('p')
    node.textContent = new Date().toLocaleTimeString()
    return node
  }
})

Move semantics: when you pass an HTMLElement, the runtime adopts it. Do not append the same element to a second content while the first drawer still owns it. Use a thunk if you need a fresh node per open.

Lazy presence: the dialog subtree unmounts on close, so any () => HTMLElement is invoked again on the next open. Use this to refresh dynamic content, or cache expensive work outside the thunk.

For the full contract, end-to-end patterns, and how content lands in the DOM, see docs/options.md → Renderable content and docs/recipes.md → Custom HTML content.


Title and Close Button

The package ships two convenience slots that cover the most common consumer needs.

Title slot

The [data-drawer-title] slot has two roles:

  • Visible title — pass a string or HTMLElement to the title option. The package renders it visibly at the top of the drawer body.
  • Accessibility target — when only ariaLabel is provided (no title), the package auto-promotes the ariaLabel value into the title slot for the aria-labelledby reference, and auto-hides the slot so the a11y text does not leak visually into the drawer. Pass an explicit titleVisuallyHidden: false to opt out of the auto-hide.
import { createDrawer } from '@samline/drawer'

// Visible title (a heading the user sees)
createDrawer({
  id: 'filters',
  title: 'Filters',
  content: 'Body'
})

// Accessibility-only title (proxy from ariaLabel, auto-hidden)
createDrawer({
  id: 'filters',
  ariaLabel: 'Filters',
  content: 'Body'
})

Built-in close button

Pass closeButton: true (or a config object) and the package renders a <button data-drawer-close> inside the drawer, wired to onOpenChange(false). This eliminates the need to write a manual document.addEventListener('click', ...) listener (which accumulates on Vite HMR cycles and triggers stale-controller bugs).

import { createDrawer } from '@samline/drawer'

const drawer = createDrawer({
  id: 'filters',
  content: 'Body',
  closeButton: {
    className: 'absolute top-5 right-5',
    icon: 'xmark', // any string; rendered as <span aria-hidden="true">
    ariaLabel: 'Close'
  }
})

The button is removed automatically on re-mount (HMR safety) and on destroyDrawer. Its click event stopPropagation()s so it does not bubble to the drawer's content.

See the close-button option shape. It is part of VanillaDrawerOptions, not a named root type export.


API at a Glance

The runtime is built around one factory plus a focused set of helpers. State mutators return snapshots, update returns a controller for the same id, and each registry helper's return shape is documented in the per-method reference.

| Group | Methods | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Factory | createDrawer · configureDrawer · createDrawerController | | Inspectors | getDrawer · getDrawers · getParentDrawer · getChildDrawers | | Mutators | updateDrawer · openDrawer · closeDrawer · toggleDrawer | | Teardown | destroyDrawer · destroyDrawers | | Properties (controller) | id · element · options | | Lifecycle (controller) | setOpen · setActiveSnapPoint · patch · update · subscribe · getSnapshot · destroy |

See the full per-method reference in docs/api/.


Presence and Mount Lifecycle

createDrawer immediately creates a dedicated <div data-drawer-vanilla-root> for that drawer in document.body or its container. An optional built-in trigger is also rendered immediately. Each drawer gets its own host, including drawers that share the same custom container.

The visual dialog uses lazy presence:

  1. Initially closed drawers have no overlay or content in the DOM. [data-drawer-overlay], [data-drawer], the handle, and the content slots mount only when the drawer opens.
  2. Closing keeps the overlay and content present for the exit. Their state changes to closed, the animation starts from the current rendered transform, and the nodes are removed after the 500 ms transition plus a 100 ms safety window. The host and optional trigger remain until destroy.
  3. open: true at creation means "open immediately." The dialog is visible without an entrance animation. For an animated programmatic open, create it closed and call setOpen(true) after mount.
  4. HMR considerations: if consumer code recreates a drawer with open: true, it is opened again on every HMR run. Prefer the closed-then-open pattern when that flash is undesirable.

Recommended pattern for "open on mount" dialogs:

import { createDrawer } from '@samline/drawer'

const drawer = createDrawer({
  id: 'my-drawer'
  // no `open` here; defaults to closed
})

// Defer open to the next microtask so the entrance animation
// runs after the mount is fully wired.
queueMicrotask(() => drawer.setOpen(true))

See CommonDrawerOptions.open for the full type contract.


Documentation

Full API reference, guides, and examples are available at samline.github.io/drawer.

| Doc | Purpose | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | docs/README.md | Overview, anatomy, and entrypoint selection. | | docs/getting-started.md | Concepts, observable contract, lifecycle, side-effect table, registry helpers. | | docs/options.md | Every CommonDrawerOptions and VanillaDrawerOptions field with defaults, behaviour, and an example per row. | | docs/recipes.md | End-to-end patterns: custom HTML content, renderable slots, lifecycle callbacks, nested drawers, snap points, scale background. | | docs/css-styling.md | The data-attribute contract the stylesheet expects; inline writes; scale ownership; position all four directions. | | docs/typescript.md | Exported types, callback signatures, helper return shapes, numeric constants, browser global type. | | docs/api/index.md | One page per public method. | | docs/vanilla.md | The root entrypoint (vanilla JS) in depth. | | docs/browser.md | Using window.Drawer with a plain <script> tag. | | CHANGELOG.md | Version history. |


License

MIT