---
name: svelte-layers
description: Svelte bindings for @stainless-code/svelte-layers (runes + stores). Use to open overlay UI imperatively from anywhere, manage ordered named stacks, choose the Svelte 5.7+ runes or Svelte 3+ stores entry, and wire layer context or stack outlets.
license: MIT
keywords:
  - tanstack-intent
  - svelte
  - modal
  - dialog
  - stack
  - typescript
metadata:
  library: "@stainless-code/svelte-layers"
  library_version: "0.2.4"
  framework: "svelte"
sources:
  - https://stainless-code.com/layers/adapters/svelte/runes
  - https://github.com/stainless-code/layers/blob/main/docs/architecture.md
---

# Svelte layer/stack UI with @stainless-code/svelte-layers

Open any layer from anywhere with `client.open()` and manage modal, dialog, drawer, popover, and toast UI as ordered, named stacks. Awaiting a typed result and fire-and-forget with `void client.open(...)` are equally first-class; named stacks, singleton `upsert` with live `update`, serial queues, nested child stacks via `useLayerGroup`, async actions via `useMutationFlow`, transitions, blockers, and validation are valuable independently.

Both entries re-export `@stainless-code/layers`, so adapter and core APIs share one import path. Use the [`layers` core skill](https://github.com/stainless-code/layers/blob/main/packages/core/skills/layers/SKILL.md) for engine behavior.

## When to use this skill

Reach for `@stainless-code/svelte-layers` to open overlay UI imperatively from anywhere and manage ordered, named stacks instead of prop-drilling, lifting state, or threading callbacks. It fits when you need any of:

- Open or close layers from anywhere.
- Await a typed result or fire-and-forget; both are first-class.
- Named stacks, singleton `upsert` with live `update`, or serial queues.
- Nested child stacks via `useLayerGroup`, transitions, dismissal guards, or validation.

Skip it only for a single, always-local overlay with no return, stacking, queue, animation, or guard needs and no wish for a global registry.

Full fit matrix: [When to use Layers](https://stainless-code.com/layers/concepts/when-to-use).

## Install

```bash
bun add @stainless-code/svelte-layers
```

Core is pulled in automatically. `svelte` is a required peer.

## Two entry points

| Entry                                 | Requires        | Pick it when                                                                              |
| ------------------------------------- | --------------- | ----------------------------------------------------------------------------------------- |
| `@stainless-code/svelte-layers`       | Svelte **5.7+** | The app uses runes and Svelte 5 reactive component syntax.                                |
| `@stainless-code/svelte-layers/store` | Svelte **3+**   | The app uses the store contract, including Svelte 3/4 or a Svelte 5 store-based codebase. |

Choose one entry for a stack surface. The runes entry depends on `createSubscriber` from `svelte/reactivity`; the stores entry depends on `readable` from `svelte/store`.

## Declare → Mount → Call

### Declare

```ts
// layers/confirm.ts
import { layerOptions } from "@stainless-code/svelte-layers";
import ConfirmDialog from "./ConfirmDialog.svelte";

export type ConfirmPayload = {
  title: string;
};

export type ConfirmResponse = boolean;

export const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
  exitingDelay: 200,
});
```

```svelte
<!-- ConfirmDialog.svelte -->
<script lang="ts">
  import type { LayerComponentProps } from "@stainless-code/svelte-layers";
  import type { ConfirmPayload, ConfirmResponse } from "./confirm";

  let { call, payload }: LayerComponentProps<ConfirmPayload, ConfirmResponse> =
    $props();
</script>

<div role="dialog">
  <h2>{payload.title}</h2>
  <button type="button" onclick={() => void call.end(true)}>Yes</button>
  <button type="button" onclick={() => void call.end(false)}>No</button>
</div>
```

### Mount — runes

```svelte
<script lang="ts">
  import { setLayerClient, useStack } from "@stainless-code/svelte-layers";
  import { confirm } from "./layers/confirm";

  setLayerClient();
  const stack = useStack({ stack: "confirm" });
</script>

{#each stack.current as s (s.id)}
  {@const call = stack.callFor(s)}
  {#if call}
    {@const Comp = confirm.component}
    <Comp
      {call}
      payload={s.payload}
      data={s.data}
      error={s.error}
      phase={s.phase}
      transition={s.transition}
      actionStatus={s.actionStatus}
      dismissing={s.dismissing}
    />
  {/if}
{/each}
```

### Mount — stores

In a store-based app, keep the declaration shape above but import `layerOptions`
from `@stainless-code/svelte-layers/store`; importing the default runes entry
requires Svelte 5.7+.

```svelte
<script lang="ts">
  import { setLayerClient, useStack, callFor } from "@stainless-code/svelte-layers/store";
  import { confirm } from "./layers/confirm";

  const client = setLayerClient();
  const stack = useStack({ stack: "confirm" });
</script>

{#each $stack as s (s.id)}
  {@const call = callFor(client, "confirm", s)}
  {#if call}
    <svelte:component
      this={confirm.component}
      {call}
      payload={s.payload}
      data={s.data}
      error={s.error}
      phase={s.phase}
      transition={s.transition}
      actionStatus={s.actionStatus}
      dismissing={s.dismissing}
    />
  {/if}
{/each}
```

The runes outlet uses the Svelte 5 dynamic-component idiom (`{@const Comp = confirm.component}` then `<Comp />`) and its stack handle's `.callFor`. The stores outlet may use `<svelte:component>` for Svelte 3/4 compatibility and the standalone `callFor`.

The component and call snippets use Svelte 5 syntax. With Svelte 3/4, use legacy `export let` props and `on:click` events; see the [package README](https://github.com/stainless-code/layers/tree/main/packages/svelte#readme) for a complete example.

### Call

```svelte
<script lang="ts">
  import { createLayer } from "@stainless-code/svelte-layers";
  import { confirm } from "./layers/confirm";

  const c = createLayer(confirm);

  async function handleRemove() {
    const ok = await c.open({ title: "Remove?" });
    if (!ok) return;
    deleteItem();
  }
</script>

<button type="button" onclick={() => void handleRemove()}>Remove</button>
```

Low-level bag-form: `useLayerClient()` + `client.open({ ...confirm, payload })`.

When `P` admits `undefined`, `payload` is optional; omit it instead of passing an empty or `undefined` value. With the default `void` response, a layer can also be fire-and-forget. Mount its `"notice"` stack with the runes outlet pattern above.

```ts
// layers/notice.ts
import { layerOptions } from "@stainless-code/svelte-layers";
import Notice from "./Notice.svelte";

export const notice = layerOptions<void>({
  stack: "notice",
  key: ["notice"],
  component: Notice,
});
```

```svelte
<!-- Notice.svelte -->
<script lang="ts">
  import type { LayerComponentProps } from "@stainless-code/svelte-layers";

  let { call }: LayerComponentProps<void> = $props();
</script>

<p>Changes saved.</p>
<button type="button" onclick={() => void call.dismiss()}>Dismiss</button>
```

```svelte
<!-- NoticeButton.svelte -->
<script lang="ts">
  import { createLayer } from "@stainless-code/svelte-layers";
  import { notice } from "./layers/notice";

  const c = createLayer(notice);
</script>

<button type="button" onclick={() => void c.open()}>
  Show notice
</button>
```

## The `call` context

Call `setLayerClient()` or `setLayerClient(existingClient)` in a parent layout/root component. Descendants use `useLayerClient()` to read that `LayerClient`; it throws when no client exists in context.

Each layer component receives:

- `call`: `end`, `dismiss`, `update`, `setRunning`, `settle`, `ended`, `index`, `stackSize`, `root`, `stackId`, `layerId`, and `addBlocker`
- state props: `payload`, `data`, `error`, `phase`, `transition`, `actionStatus`, and `dismissing`

`await call.end(response)` resolves the caller's `await` and dismisses the layer. It returns `Promise<boolean>`; `false` means a blocker vetoed dismissal.

`key` is logical identity for operations such as `find`, `upsert`, and `gcTime`; every mount gets a unique `id`. Key `{#each}` blocks with `s.id`, because parallel stacks may contain multiple same-key layers.

### Transitions

Key CSS on `transition`; the configured delay or `call.settle()` completes the transition, whichever happens first:

```svelte
<div
  role="dialog"
  data-transition={transition}
  ontransitionend={() => call.settle()}
>
  <h2>{payload.title}</h2>
</div>
```

Set `enteringDelay` / `exitingDelay` on `layerOptions`. For spring or variable-duration exits, combine a generous `exitingDelay` cap with `ontransitionend`.

### Rendering location

The adapter has no built-in outlet or portal host. Render active layers inline in a layout with `useStack().current` and `callFor`, or move them with the app's portal pattern; nested child stacks from `useLayerGroup` follow the same render path.

## Wired handle: createLayer

```ts
const c = createLayer(confirm);
await c.open({ title: "Remove?" });
```

Core headless factory is `createLayerHandle`.

## Subscribing to stacks

Options-bag + optional trailing `client`; `select` (not `selector`).

### useStack / createQueuedStack

Runes return a `SvelteStack` handle: read `.current` in a reactive context.

```svelte
<script lang="ts">
  import { useStack, createQueuedStack } from "@stainless-code/svelte-layers";

  const stack = useStack({ stack: "confirm" });
  const count = useStack({ stack: "confirm", select: (s) => s.length });
  const queued = createQueuedStack({ stack: "confirm" });
</script>

<p>{stack.current.length} open</p>
```

### createLayerState / createLayerQueuedState

Observe-only; `.current` is `LayerState[]` for all same-key instances:

```svelte
<script lang="ts">
  import { createLayerState } from "@stainless-code/svelte-layers";
  import { confirm } from "./layers/confirm";

  const mounted = createLayerState({ key: confirm.key, stack: "confirm" });
</script>

{#if mounted.current.at(-1)}
  <span>{mounted.current.at(-1)!.payload.title}</span>
{/if}
```

Stores entry: same names (`createLayer`, `createLayerState`, `createQueuedStack`, `createLayerQueuedState`); `useStack({ … })` returns `Readable<T>` — auto-subscribe with `$stack`.

## Mutation flow

`useMutationFlow(call)` coordinates a layer's `actionStatus: "running"` with async work. `run(fn).orEnd(response)` ends the layer on success; on failure it stays open and the error rethrows.

### Runes

`pending` is a reactive `readonly boolean` — read `flow.pending` in markup.

```svelte
<script lang="ts">
  import { type LayerComponentProps, useMutationFlow } from "@stainless-code/svelte-layers";

  let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props();
  const flow = useMutationFlow(call);
</script>

<div role="dialog" aria-label={payload.title}>
  <button
    type="button"
    disabled={flow.pending}
    onclick={() => void flow.run(() => save()).orEnd(true)}
  >
    Save
  </button>
</div>
```

### Stores

`pending` is a `Readable<boolean>` — destructure from the hook and subscribe with `$pending`.

```svelte
<script lang="ts">
  import { type LayerComponentProps, useMutationFlow } from "@stainless-code/svelte-layers/store";

  let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props();
  const { pending, run } = useMutationFlow(call);
</script>

<div role="dialog" aria-label={payload.title}>
  <button type="button" disabled={$pending} onclick={() => void run(() => save()).orEnd(true)}>
    Save
  </button>
</div>
```

## Nested stacks

`useLayerGroup(call, options?)` creates a child stack scoped to the calling layer's lifetime. Returns `{ open, dismissAll, stack, stackId }`. On `onDestroy`, the group `dispose`s and `cancelAll`s child layers (`LayerCancelledError`). Render child layers inline with the group's stack handle.

### Runes

`stack` is a `SvelteStack` — `{#each group.stack.current as s}` + `group.stack.callFor(s)`.

```svelte
<script lang="ts">
  import { type LayerComponentProps, useLayerGroup } from "@stainless-code/svelte-layers";
  import { childLayer } from "./layers/child";

  let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props();
  const group = useLayerGroup(call);
</script>

<div role="dialog" aria-label={payload.title}>
  <button
    type="button"
    onclick={() =>
      void group.open({ ...childLayer, payload: { label: "Child" } })}
  >
    Open child
  </button>
  {#each group.stack.current as s (s.id)}
    {@const childCall = group.stack.callFor(s)}
    {#if childCall}
      {@const Comp = childLayer.component}
      <Comp call={childCall} payload={s.payload} phase={s.phase} actionStatus={s.actionStatus} />
    {/if}
  {/each}
</div>
```

### Stores

`stack` is a `Readable<LayerState[]>` — `{#each $stack}` + `callFor(client, group.stackId, s)`.

```svelte
<script lang="ts">
  import {
    callFor,
    type LayerComponentProps,
    useLayerClient,
    useLayerGroup,
  } from "@stainless-code/svelte-layers/store";
  import { childLayer } from "./layers/child";

  let { call, payload }: LayerComponentProps<{ title: string }, boolean> = $props();
  const client = useLayerClient();
  const group = useLayerGroup(call);
  const stack = group.stack;
</script>

<div role="dialog" aria-label={payload.title}>
  <button
    type="button"
    onclick={() =>
      void group.open({ ...childLayer, payload: { label: "Child" } })}
  >
    Open child
  </button>
  {#each $stack as s (s.id)}
    {@const childCall = callFor(client, group.stackId, s)}
    {#if childCall}
      <svelte:component
        this={childLayer.component}
        call={childCall}
        payload={s.payload}
        phase={s.phase}
        actionStatus={s.actionStatus}
      />
    {/if}
  {/each}
</div>
```

## Adapter API

| API                      | Runes entry                                                             | Stores entry                                                                       | Role                                                                        |
| ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `setLayerClient`         | `(client?) => LayerClient`                                              | Same                                                                               | Put a provided or new client in Svelte context.                             |
| `useLayerClient`         | `() => LayerClient`                                                     | Same                                                                               | Read the nearest client; throws when absent.                                |
| `createLayer`            | `({ … }, client?) => SvelteStack` wired handle + state/queued/top       | Same                                                                               | Drive + observe; `open(payload)` — no spread.                               |
| `createLayerState`       | `({ key, … }, client?) => SvelteStack<LayerState[]>`                    | `Readable<LayerState[]>`                                                           | Observe-only, mounted.                                                      |
| `createLayerQueuedState` | `({ key, … }, client?) => SvelteStack<LayerState[]>`                    | `Readable<LayerState[]>`                                                           | Observe-only, queued.                                                       |
| `useStack`               | `({ stack?, select?, compare? }, client?) => SvelteStack`               | `Readable<T>`                                                                      | Whole-stack mounted.                                                        |
| `createQueuedStack`      | `({ stack?, select?, compare? }, client?) => SvelteStack`               | `Readable<T>`                                                                      | Whole-stack queued.                                                         |
| `useMutationFlow`        | `(call) => { readonly pending: boolean, run }`                          | `(call) => { pending: Readable<boolean>, run }`                                    | Drive `actionStatus: "running"`; `run(fn).orEnd(response)` ends on success. |
| `useLayerGroup`          | `(call, options?) => { open, dismissAll, stack: SvelteStack, stackId }` | `(call, options?) => { open, dismissAll, stack: Readable<LayerState[]>, stackId }` | Child stack scoped to the calling layer; cleaned up on `onDestroy`.         |
| `SvelteStack`            | `{ readonly current: T; callFor(state, rootProps?) }`                   | —                                                                                  | Reactive runes handle.                                                      |
| `callFor`                | —                                                                       | `(client, stackId, state, rootProps?) => LayerCallContext \| null`                 | Build a call context; returns `null` if the layer is gone.                  |
| `MutationFlow<R>`        | `{ readonly pending: boolean, run }`                                    | `{ pending: Readable<boolean>, run }`                                              | `pending` mirrors `actionStatus: "running"`.                                |
| `MutationRun<R>`         | `{ orEnd(response) => Promise<void> }`                                  | Same                                                                               | Ends on success; on failure leaves the layer open and rethrows.             |
| `LayerGroup`             | `{ open, dismissAll, stack: SvelteStack, stackId }`                     | `{ open, dismissAll, stack: Readable<LayerState[]>, stackId }`                     | `open` is stack-pre-bound; `dismissAll(response?)` clears the child stack.  |

## Core re-exports (same import path)

Both entries use `export * from "@stainless-code/layers"` — includes `createLayer`, `LayerHandle`, `ValidatedLayerHandle`, plus adapter types (`LayerComponentProps`, `SvelteStack`, `MutationFlow<R>`, …).

Common core patterns:

- **Key inference:** `layerOptions` / `layerKey` branding makes `await client.open(...)` infer its response.
- **Singleton and live updates:** set `upsert: true` on `open`; update with `client.getStack(id).update(layer, patch)`.
- **Serial scope:** configure `new LayerClient({ defaultStackOptions: { confirm: { scope: { strategy: "serial" } } } })`.
- **Validation:** set `validate` on `layerOptions` or `open`; narrow `PayloadValidationError` with `isPayloadValidationError`.
- **Blockers:** use `call.addBlocker` / `stack.addBlocker`, `dismissing`, and `dismissAll` modes.
- **Nested stacks:** `useLayerGroup(call, options?)` wraps core `createLayerGroup` with Svelte lifetime cleanup; `childStackId` remains available for manual wiring.

See the [`layers` core skill](https://github.com/stainless-code/layers/blob/main/packages/core/skills/layers/SKILL.md) for full engine coverage, the [architecture guide](https://github.com/stainless-code/layers/blob/main/docs/architecture.md) for package boundaries, and the [repository README](https://github.com/stainless-code/layers#readme) for project-level setup.
