Modals are just async functions you forgot to await.
Headless layer/stack manager — modals, dialogs, drawers, popovers, toasts are layers in named stacks. Open any layer from anywhere and await a typed result. A zero-dependency core plus React, Preact, Solid, Angular, Vue, Lit, Alpine, and Svelte adapters. State coordination, not UI ownership: Layers owns ordering, keys, transitions, blockers, and the await client.open(...) contract; you own rendering, focus, portals, and a11y.
Experimental — the API may change between minor releases. Pin your version. (Stability & versioning)
Full docs: stainless-code.com/layers.
| Package | Peer | Install |
|---|---|---|
@stainless-code/layers |
— | bun add @stainless-code/layers |
@stainless-code/react-layers |
react |
bun add @stainless-code/react-layers |
@stainless-code/preact-layers |
preact |
bun add @stainless-code/preact-layers |
@stainless-code/solid-layers |
solid-js |
bun add @stainless-code/solid-layers |
@stainless-code/angular-layers |
@angular/core |
bun add @stainless-code/angular-layers |
@stainless-code/vue-layers |
vue |
bun add @stainless-code/vue-layers |
@stainless-code/lit-layers |
lit, @lit/context |
bun add @stainless-code/lit-layers |
@stainless-code/alpine-layers |
alpinejs |
bun add @stainless-code/alpine-layers |
@stainless-code/svelte-layers |
svelte |
bun add @stainless-code/svelte-layers |
@stainless-code/layers-devtools |
— (Solid panel) | bun add -d @stainless-code/layers-devtools |
@stainless-code/react-layers-devtools |
react, @stainless-code/react-layers, @tanstack/react-devtools |
bun add -d @stainless-code/react-layers-devtools |
Install only the adapter for your framework — it pulls the core in transitively and re-exports it, so adapter APIs (StackProvider, useStack, …) and core APIs (LayerClient, layerOptions, …) both come from the one package. Each adapter lists its framework as a required peer. Svelte ships two entries: @stainless-code/svelte-layers (runes, 5.7+) and @stainless-code/svelte-layers/store (stores, 3+). Devtools packages are optional; see Devtools.
import {
layerOptions,
StackProvider,
StackOutlet,
useLayer,
type LayerComponentProps,
} from "@stainless-code/react-layers";
type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;
function ConfirmDialog({
call,
payload,
}: LayerComponentProps<ConfirmPayload, ConfirmResponse>) {
return (
<div role="dialog">
<h2>{payload.title}</h2>
<button onClick={() => void call.end(true)}>Yes</button>
<button onClick={() => void call.end(false)}>No</button>
</div>
);
}
const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
stack: "confirm",
key: ["confirm", "remove"],
component: ConfirmDialog,
});
function App() {
return (
<StackProvider>
<StackOutlet stack="confirm" />
</StackProvider>
);
}
function useRemove() {
const c = useLayer(confirm);
return async () => {
const ok = await c.open({ title: "Remove?" });
// ^? boolean
};
}useLayer(options)— wired handle (drive + observe).useLayerState/useLayerQueuedState/useQueuedStack— observe-only. CorecreateLayer(options, client)for headless callers. See Concepts.
This is a bun workspaces monorepo; root scripts fan out across packages with bun run --filter '*' <script>.
bun install
bun run check # build (core-first) + format + lint + test + test:dom + typecheckSee .github/CONTRIBUTING.md to contribute and AGENTS.md for agent instructions.
MIT.