Skip to content
Layers
Esc
navigateopen⌘Jpreview

Open any layerfrom anywhere.

Stop prop-drilling isOpen and threading onConfirm callbacks through your tree.

Modals, drawers, toasts, and confirms as named, ordered stacks — open one from any component, effect, or route guard and await a typed result. Headless core, eight framework adapters.

8
adapters
8
live examples
zero-dep
core
import { LayerClient, createLayer, layerOptions } from "@stainless-code/layers";

const client = new LayerClient();

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: renderConfirm, // your render fn
});

const c = createLayer(confirm, client);

async function remove() {
  const ok = await c.open({ title: "Remove?" });
  //    ^? boolean
}
bun add @stainless-code/layers

Try it live.

Modals are just async functions you forgot to await. Open a confirm, toast, or serial flow below and watch the stack and lifecycle update live in the inspector.

await boolean result
Layer inspectordevtools
Stacks (8)
hero-confirm
scope:paralleldismissAll:skipBlockedgcTime:0ms
Mounted (0)Queued (0)
empty — run a demo
dismissAll modes

When to use it

When to reach for Layers.

Overlay state is cross-cutting, but component models push it local. Layers moves it into a client you invoke imperatively — declare once, open from anywhere.

Use caseWhat it involvesFit
Confirm/prompt that returns a valueawait open + DataTag response inferenceIdeal
Open an overlay from non-UI code (route guard, event bus, effect, util)global LayerClient.openIdeal
Toast / snackbar / progress — one instance, updated liveupsert + updateIdeal
One-at-a-time / sequential flows (onboarding, queued confirms)serial scope + getQueuedSnapshotIdeal
Stacked or nested overlays (drawer → sub-dialog)layer groups (useLayerGroup / createLayerGroup)Ideal
Guard dismissal ("discard unsaved changes?")blockers (addBlocker, dismissing, dismissAll)Ideal
Enter/exit animated overlaystransition + enteringDelay/exitingDelay settle()Good
Validate an untrusted payload at openvalidate (Standard Schema or a function)Nice-to-have
One overlay system across React/Preact/Solid/Angular/Vue/Lit/Alpine/SvelteUI-agnostic core + adaptersIdeal

When to skip it.

A single local dialog with no return value is simpler as a controlled component. Layers pays for itself when overlay state crosses boundaries, stacks, queues, or needs a typed await.

Use caseFit
A single, always-local overlay — no return, stacking, queue, animation, or guard, and no wish for a global registrySkip → plain controlled component
Static/inline content with no overlay semantics; full-page navigationSkip → plain markup / a router

One core, eight adapters.

Declare a layer once, then open and await it — same engine, your framework's reactivity.

import { LayerClient, createLayer, layerOptions } from "@stainless-code/layers";

const client = new LayerClient();

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: renderConfirm, // your render fn
});

const c = createLayer(confirm, client);

async function remove() {
  const ok = await c.open({ title: "Remove?" });
  //    ^? boolean
}
import {
  layerOptions,
  StackProvider,
  StackOutlet,
  useLayer,
} from "@stainless-code/react-layers";

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});

function App() {
  return (
    <StackProvider>
      <StackOutlet stack="confirm" />
      <RemoveButton />
    </StackProvider>
  );
}

function RemoveButton() {
  const c = useLayer(confirm);
  async function remove() {
    const ok = await c.open({ title: "Remove?" });
    //    ^? boolean
  }
  return <button onClick={() => void remove()}>Remove</button>;
}
import {
  layerOptions,
  StackProvider,
  StackOutlet,
  useLayer,
} from "@stainless-code/preact-layers";

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});

function App() {
  return (
    <StackProvider>
      <StackOutlet stack="confirm" />
      <RemoveButton />
    </StackProvider>
  );
}

function RemoveButton() {
  const c = useLayer(confirm);
  async function remove() {
    const ok = await c.open({ title: "Remove?" });
    //    ^? boolean
  }
  return <button onClick={() => void remove()}>Remove</button>;
}
import {
  LayerClient,
  LayerClientContext,
  StackOutlet,
  layerOptions,
  useLayer,
} from "@stainless-code/solid-layers";

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});

const client = new LayerClient();

function App() {
  return (
    <LayerClientContext.Provider value={client}>
      <StackOutlet stack="confirm" />
      <RemoveButton />
    </LayerClientContext.Provider>
  );
}

function RemoveButton() {
  const c = useLayer(confirm);
  async function remove() {
    const ok = await c.open({ title: "Remove?" });
    //    ^? boolean
  }
  return <button onClick={() => void remove()}>Remove</button>;
}
import { Component, ViewContainerRef, inject } from "@angular/core";
import {
  layerOptions,
  provideLayerClient,
  renderStack,
  injectLayer,
} from "@stainless-code/angular-layers";

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialogComponent,
});

@Component({
  selector: "app-root",
  providers: [provideLayerClient()],
  template: `<button (click)="remove()">Remove</button><ng-container #outlet />`,
})
export class AppComponent {
  private c = injectLayer(confirm);
  private vcr = inject(ViewContainerRef);

  constructor() {
    renderStack(this.vcr, "confirm");
  }

  async remove() {
    const ok = await this.c.open({ title: "Remove?" });
    //    ^? boolean
  }
}
<script setup lang="ts">
import {
  layerOptions,
  provideLayerClient,
  StackOutlet,
  useLayer,
} from "@stainless-code/vue-layers";

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});

provideLayerClient();
const c = useLayer(confirm);

async function remove() {
  const ok = await c.open({ title: "Remove?" });
  //    ^? boolean
}
</script>

<template>
  <StackOutlet stack="confirm" />
  <button @click="remove">Remove</button>
</template>
import {
  createLayer,
  defineStackElements,
  layerOptions,
  provideLayerClient,
  useLayer,
} from "@stainless-code/lit-layers";
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";

defineStackElements();

const confirm = layerOptions({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});

@customElement("app-shell")
class AppShell extends LitElement {
  // Field order: provide before useLayer (same-host context is not the path).
  #client = provideLayerClient(this);
  #confirm = useLayer(this, confirm, this.#client);

  createRenderRoot() {
    return this;
  }

  render() {
    return html`
      <button
        type="button"
        @click=${() => void this.#confirm.open({ title: "Remove?" })}
      >
        Remove
      </button>
      <stack-outlet stack="confirm"></stack-outlet>
    `;
  }
}
<script type="module">
  import Alpine from "alpinejs";
  import layers, {
    createLayer,
    layerOptions,
    setLayerClient,
  } from "@stainless-code/alpine-layers";

  Alpine.plugin(layers);
  setLayerClient();

  const confirm = layerOptions<{ title: string }, boolean>({
    stack: "confirm",
    key: ["confirm", "remove"],
  });

  const c = createLayer(confirm);

  Alpine.data("demo", () => ({
    async remove() {
      const ok = await c.open({ title: "Remove?" });
      //    ^? boolean
    },
  }));

  window.Alpine = Alpine;
  Alpine.start();
</script>

<div x-data="demo">
  <template x-layer-outlet="'confirm'">
    <div role="dialog">
      <h2 x-text="$layer.payload.title"></h2>
      <button type="button" @click="$layer.call.end(true)">Yes</button>
    </div>
  </template>
  <button type="button" @click="remove()">Remove</button>
</div>
<script lang="ts">
  import {
    createLayer,
    layerOptions,
    setLayerClient,
    useStack,
  } from "@stainless-code/svelte-layers";

  const confirm = layerOptions({
    stack: "confirm",
    key: ["confirm", "remove"],
  });

  setLayerClient();
  const stack = useStack({ stack: "confirm" });
  const c = createLayer(confirm);

  async function remove() {
    const ok = await c.open({ title: "Remove?" });
    //    ^? boolean
  }
</script>

{#each stack.current as state (state.id)}
  {@const call = stack.callFor(state)}
  <!-- render your dialog with call + state.payload -->
{/each}

<button onclick={remove}>Remove</button>
<script lang="ts">
  import {
    callFor,
    createLayer,
    layerOptions,
    setLayerClient,
    useLayerClient,
    useStack,
  } from "@stainless-code/svelte-layers/store";

  const confirm = layerOptions({
    stack: "confirm",
    key: ["confirm", "remove"],
  });

  setLayerClient();
  const client = useLayerClient();
  const stack = useStack({ stack: "confirm" });
  const c = createLayer(confirm);

  async function remove() {
    const ok = await c.open({ title: "Remove?" });
    //    ^? boolean
  }
</script>

{#each $stack as state (state.id)}
  {@const call = callFor(client, "confirm", state)}
  <!-- render your dialog with call + state.payload -->
{/each}

<button onclick={remove}>Remove</button>

Browse adapter guides →

Svelte ships two modes — runes and store.

Every adapter, the same API.

React, Preact, Solid, Angular, Vue, Lit, and Alpine re-export the full core; Svelte re-exports selectively. Angular, Alpine, and Svelte diverge by design — imperative / markup outlets instead of React-shaped hosts.

CapabilityReactPreactSolidAngularVueLitAlpineSvelte (runes)Svelte (store)
Primitives + core re-export
StackOutlet (built-in host)øøø
useLayerGroup
useMutationFlow
createStackHookøø

Full parity matrix →

Batteries included.

Handle the overlay cases that stop being local: ordered stacks, one-at-a-time flows, nested dialogs, guarded closes, and validated input. Adopt only the pieces your UI needs.

Start opening layers.

Install one adapter — the core ships as its dependency and is re-exported.

bun add @stainless-code/layers