TypeScript reference
@samline/drawer is written in strict TypeScript. The root entrypoint exports the types below; browser-global typing is intentionally separate.
Root type exports
Section titled “Root type exports”import type { CommonDrawerController, CommonDrawerDirection, CommonDrawerId, CommonDrawerOptions, CommonDrawerSnapshot, CommonDrawerSnapPoint, VanillaDrawerController, VanillaDrawerOptions, VanillaRenderable} from '@samline/drawer'CommonDrawerState, VanillaCloseButtonOptions, and DrawerApi are not root named type exports. Their usable forms are documented below.
CommonDrawerDirection
Section titled “CommonDrawerDirection”type CommonDrawerDirection = 'top' | 'bottom' | 'left' | 'right'All four values support entrance/exit motion, snap math, and drag-to-dismiss. Closing gestures are up for top, down for bottom, left for left, and right for right.
CommonDrawerSnapPoint
Section titled “CommonDrawerSnapPoint”type CommonDrawerSnapPoint = number | stringNumbers are fractions of the viewport or custom container. Strings are parsed as absolute pixel counts, so '120px' resolves to 120 pixels and '50%' is parsed as 50 pixels rather than 50 percent.
CommonDrawerId
Section titled “CommonDrawerId”type CommonDrawerId = stringThe registry key. Reusing an id merges options into the same runtime instance and per-id host.
CommonDrawerOptions
Section titled “CommonDrawerOptions”See Configuration for defaults and detailed behavior.
interface CommonDrawerOptions { id?: CommonDrawerId parentId?: CommonDrawerId open?: boolean defaultOpen?: boolean onOpenChange?: (open: boolean) => void onClose?: () => void onAnimationEnd?: (open: boolean) => void onActiveSnapPointChange?: (snapPoint: CommonDrawerSnapPoint | null) => void onDragChange?: (percentageDragged: number) => void onReleaseChange?: (open: boolean) => void dismissible?: boolean modal?: boolean nested?: boolean direction?: CommonDrawerDirection snapPoints?: CommonDrawerSnapPoint[] fadeFromIndex?: number activeSnapPoint?: CommonDrawerSnapPoint | null closeThreshold?: number scrollLockTimeout?: number shouldScaleBackground?: boolean setBackgroundColorOnScale?: boolean handleOnly?: boolean fixed?: boolean disablePreventScroll?: boolean repositionInputs?: boolean snapToSequentialPoint?: boolean preventScrollRestoration?: boolean noBodyStyles?: boolean autoFocus?: boolean preventCycle?: boolean}CommonDrawerSnapshot
Section titled “CommonDrawerSnapshot”interface CommonDrawerSnapshot { options: CommonDrawerOptions state: { isOpen: boolean activeSnapPoint: CommonDrawerSnapPoint | null direction: CommonDrawerDirection snapPoints: CommonDrawerSnapPoint[] dismissible: boolean modal: boolean }}The source names the nested shape CommonDrawerState, but the root package does not re-export that name. Derive it without relying on an unavailable import:
import type { CommonDrawerSnapshot } from '@samline/drawer'
type CommonDrawerState = CommonDrawerSnapshot['state']CommonDrawerController
Section titled “CommonDrawerController”interface CommonDrawerController { getSnapshot: () => CommonDrawerSnapshot setOpen: (open: boolean) => CommonDrawerSnapshot setActiveSnapPoint: (snapPoint: CommonDrawerSnapPoint | null) => CommonDrawerSnapshot patch: (options: Partial<CommonDrawerOptions>) => CommonDrawerSnapshot subscribe: (listener: (snapshot: CommonDrawerSnapshot) => void) => () => void}getSnapshot()synchronously reads state.setOpen(open)publishes and returns the resulting snapshot.setActiveSnapPoint(value)updates the point and returns the snapshot; it does not echoonActiveSnapPointChange.patch(options)shallow-merges options, publishes, and returns the snapshot.subscribe(listener)invokes the listener immediately and returns an unsubscribe function.
import { createDrawerController } from '@samline/drawer'
const controller = createDrawerController({ id: 'filters', direction: 'bottom', defaultOpen: true})
controller.getSnapshot().state.isOpen // truecontroller.setOpen(false)const unsubscribe = controller.subscribe((snapshot) => { console.log(snapshot.state.isOpen)})unsubscribe()VanillaDrawerController
Section titled “VanillaDrawerController”interface VanillaDrawerController extends CommonDrawerController { id: CommonDrawerId element: HTMLElement | null options: VanillaDrawerOptions update: (options?: VanillaDrawerOptions) => VanillaDrawerController destroy: () => void}idis the normalized registry id.elementis the dedicated[data-drawer-vanilla-root]host, not the lazy[data-drawer]dialog. It exists while the id is registered in a DOM environment, including while closed, and becomesnullafter destroy.optionsis the latest shallow-merged vanilla option object.update(options?)delegates tocreateDrawer({ ...options, id })and returns a controller wrapper for the same underlying instance.destroy()delegates todestroyDrawer(id).
import { createDrawer } from '@samline/drawer'
const drawer = createDrawer({ id: 'filters', title: 'Filters' })drawer.element // <div data-drawer-vanilla-root="filters">drawer.id // 'filters'drawer.options // { id: 'filters', title: 'Filters', ... }
drawer.update({ activeSnapPoint: '420px' })drawer.destroy()drawer.element // nullgetDrawer() and getDrawers() can return fresh controller wrapper objects. Compare ids or state, not object identity; all wrappers for one live id target the same underlying controller.
VanillaDrawerOptions
Section titled “VanillaDrawerOptions”CommonDrawerOptions extended with the vanilla-only host / trigger / handle / content / className options. See Configuration → Vanilla-only fields for the full list with examples.
interface VanillaDrawerOptions extends CommonDrawerOptions { container?: HTMLElement | null /** @deprecated Use container. */ mountElement?: HTMLElement | null triggerElement?: HTMLElement | null triggerText?: string showHandle?: boolean handleClassName?: string ariaLabel?: string ariaLabelledBy?: string ariaDescribedBy?: string title?: VanillaRenderable titleVisuallyHidden?: boolean description?: VanillaRenderable descriptionVisuallyHidden?: boolean content?: VanillaRenderable overlayClassName?: string contentClassName?: string closeButton?: | boolean | { className?: string icon?: string | HTMLElement ariaLabel?: string }}container is preferred. mountElement is deprecated and only used as a fallback when container is nullish.
Close-button object shape
Section titled “Close-button object shape”The source interface is named VanillaCloseButtonOptions, but that name is not re-exported from the package root. Derive the object branch from VanillaDrawerOptions:
import type { VanillaDrawerOptions } from '@samline/drawer'
type CloseButtonOptions = Exclude<NonNullable<VanillaDrawerOptions['closeButton']>, boolean>
const closeButton: CloseButtonOptions = { className: 'drawer-close', icon: 'xmark', ariaLabel: 'Close filters'}The defaults are drawer-close-button, xmark, and Close respectively.
VanillaRenderable
Section titled “VanillaRenderable”type VanillaRenderable = string | number | HTMLElement | (() => HTMLElement) | null | undefinedStrings and numbers become text nodes. An element is moved into the dialog. A thunk is invoked once per dialog DOM build and must return an HTMLElement; lazy Presence means closing removes that element and reopening builds the slots again.
import { createDrawer } from '@samline/drawer'
// 1. StringcreateDrawer({ id: 'a', content: 'Hello' })
// 2. NumbercreateDrawer({ id: 'b', title: 3 })
// 3. Pre-built elementconst form = document.createElement('form')createDrawer({ id: 'c', content: form })
// 4. Lazy thunkcreateDrawer({ id: 'd', content: () => { const node = document.createElement('div') node.textContent = new Date().toLocaleTimeString() return node }})
// 5. EmptycreateDrawer({ id: 'e' })Factory returns
Section titled “Factory returns”function createDrawer(options?: VanillaDrawerOptions): VanillaDrawerControllerfunction createDrawerController(options?: CommonDrawerOptions): CommonDrawerControllercreateDrawer registers and renders a per-id host. createDrawerController is headless: it publishes snapshots but does not mount DOM or run registry lifecycle callbacks/effects.
Numeric constants
Section titled “Numeric constants”These values are root runtime exports, not type-only declarations:
| Constant | Value | Runtime use |
|---|---|---|
TRANSITIONS.DURATION |
0.5 |
Transition duration in seconds and lifecycle timer basis. |
TRANSITIONS.EASE |
[0.32, 0.72, 0, 1] |
Transform/opacity easing curve. |
VELOCITY_THRESHOLD |
0.4 |
Velocity threshold used by release decisions. |
CLOSE_THRESHOLD |
0.25 |
Default dismissed fraction of the rendered drawer dimension. |
SCROLL_LOCK_TIMEOUT |
100 |
Drag cooldown after scrollable content prevents a gesture. |
BORDER_RADIUS |
8 |
Open-rest page-wrapper radius for background scaling. |
NESTED_DISPLACEMENT |
16 |
Parent displacement used by nested-drawer transforms. |
WINDOW_TOP_OFFSET |
26 |
Background base-scale and mobile viewport offset input. |
DRAG_CLASS |
'drawer-dragging' |
Class added after axis intent is accepted and removed on release/cancel. |
import { CLOSE_THRESHOLD, TRANSITIONS, VELOCITY_THRESHOLD } from '@samline/drawer'
console.log(TRANSITIONS.DURATION) // 0.5console.log(TRANSITIONS.EASE) // [0.32, 0.72, 0, 1]console.log(VELOCITY_THRESHOLD) // 0.4console.log(CLOSE_THRESHOLD) // 0.25Browser global type
Section titled “Browser global type”DrawerApi is exported by the @samline/drawer/browser declarations, not by the root entrypoint. Keep the import type-only so no runtime browser bundle is imported:
import type { DrawerApi } from '@samline/drawer/browser'
declare global { interface Window { Drawer?: DrawerApi }}
window.Drawer?.createDrawer({ id: 'filters', title: 'Filters', content: 'Body' })The interface uses the same function types as the root named API, including both updateDrawer(options) and updateDrawer(id, options) forms. There is no root runtime export named browser or root type export named DrawerApi.
Subscribing
Section titled “Subscribing”import { createDrawer } from '@samline/drawer'
const drawer = createDrawer({ id: 'filters', title: 'Filters' })const unsubscribe = drawer.subscribe((snapshot) => { console.log('isOpen:', snapshot.state.isOpen) console.log('snap:', snapshot.state.activeSnapPoint)})
unsubscribe()The listener runs immediately and on each controller publication. Use getSnapshot() for a synchronous read without subscribing.
import { createDrawer } from '@samline/drawer'
const drawer = createDrawer({ id: 'filters' })const snap = drawer.getSnapshot()if (snap.state.isOpen) { // safe to read state synchronously}setOpen(), setActiveSnapPoint(), and patch() all return the new snapshot, so you can chain state changes without an extra getSnapshot() call.
import { createDrawer } from '@samline/drawer'
const drawer = createDrawer({ id: 'filters' })const next = drawer.patch({ title: 'Filters', activeSnapPoint: '420px' }).setOpen(true)console.log(next.state.isOpen) // true