Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

Overview

Overlay state is cross-cutting; a zero-dep headless engine plus framework adapters split it from UI rendering.

A confirm, toast, or drawer rarely stays where it started — prop-drilled isOpen and threaded callbacks don’t scale. Layers moves that into a headless client: declare once, mount an outlet, await confirm.open(...) from any call site (wired handle or bag-form). Zero-dep core; thin adapters bind each framework’s reactivity.

Engine model

LayerClient ──┬── LayerStack (named, ordered) ── Layer[]
              └── LayerClientOptions.defaultStackOptions
  1. DeclarelayerOptions<P, R>({ stack, key, ... }) brands the keyThe logical identity of a layer; find/upsert/gcTime operate on its signature. with a DataTag so open() infers the response type.
  2. Observe — subscribe to a stack snapshot; adapters bind LayerStack.subscribe / getSnapshot to UI reactivity.
  3. Call — a wired handle’s open(payload) (or bag-form client.open({ ...options, payload })); resolution happens when something calls call.end / call.dismiss (response optional when undefined extends R).

Package boundary

The seam is a published package boundary in a bun-workspaces monorepo: one zero-dep core package plus one adapter package per library or framework.

Core (@stainless-code/layers)

Zero runtime dependencies; sideEffects: false.

EngineLayerClient (ensureStack, getStackIds, subscribeStacks) owns app-wide stacks; LayerStack (dismiss, dismissAll, settle, setRunning, cancelQueued, getQueuedSnapshot) is one named, ordered stack; Layer is a single instance in that stack.

Declare & inferlayerOptions, layerKey / DataTag brand keys so open() infers payload and response types without explicit generics.

Lifecyclephase, transition, actionStatus are three independent axes; createCallContext is the per-layer handle (end, dismiss, settle, setRunning, addBlocker).

Stacks & scopenamed stacks, serial/parallel scope, upsert, gcTime; createLayerGroup / childStackId for nested child stacks.

BlockersA consumer predicate that gates dismissal; true allows, falsy or a thrown error vetoes. — instance- and stack-scoped addBlocker gate user-intent dismissal; dismissAll supports skipBlocked / stopAtBlocked / force modes.

Types & validationRegister / DefaultLayerError for app-wide error typing; payload validation via StandardSchemaV1, Validator, PayloadValidationError, isPayloadValidationError (payload validation).

InternalsSubscribable, notifyManager (batched mutations → one re-render); ControlledPromise for awaitable open resolution; hashKey, keySignature utilities.

Adapters (@stainless-code/<fw>-layers)

Each adapter peers its framework, depends on @stainless-code/layers (re-exported), and ships client context + useStack / wired-handle hooks plus a render surface (StackOutlet or the imperative/primitive equivalent — convenience, not required). Install one adapter package; core comes transitively. Svelte: . (runes, 5.7+) and ./store (stores, 3+).

Subscription model

Adapters subscribe to LayerStack.getSnapshot() and project via a selector. On React and Preact, useStack({ stack, select, compare }) wraps useSyncExternalStore(stack.subscribe, getSnapshot, getServerSnapshot). The snapshot is cached on LayerStack (stable ref between mutations); the hook memoizes selector output with compare-equality (default Object.is) so object/array selectors don’t churn.

Last updated on August 8, 2026

Was this page helpful?