Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

Nested overlays

Child stacks scoped to a parent layer via useLayerGroup or createLayerGroup.

A drawer that opens a sub-dialog needs its own stack — but tied to the parent layer’s lifetime. Layer groups create a child stack on the same LayerClient; when the parent dismisses, children are cleared with cancelAll and awaiting child open() promises reject with LayerCancelledError (not a completion response). Narrow with isLayerCancelledError — see Error handling. void group.open(...) is fine (core swallows cancel for fire-and-forget).

Create a child stack

createLayerGroup(client, call, options?) registers a child stack derived from the parent’s stackId and instance layerId. Adapters wrap it as useLayerGroup(call) (Lit: useLayerGroup(host, call, …)):

import { createLayerGroup } from "@stainless-code/layers";

// headless / custom adapter
const group = createLayerGroup(client, call);
void group.open({ ...nestedConfirm, payload });
group.dismissAll();
group.dispose(); // usually automatic via adapter lifecycle

Render the child stack inline

Render the child stack inside the parent layer’s DOM. React, Preact, Solid, Angular, Vue, Lit, Alpine, and Svelte each expose a child render path — Outlet, renderInto, outlet(), nested x-layer-outlet, or the child stack (tabs below):

import { useLayerGroup } from "@stainless-code/react-layers";

function Drawer({ call }: LayerComponentProps<void, void>) {
  const group = useLayerGroup(call);
  return (
    <div role="dialog">
      <button onClick={() => void group.open({ ...nestedConfirm, payload })}>
        Edit details
      </button>
      <group.Outlet />
    </div>
  );
}
import { useLayerGroup } from "@stainless-code/preact-layers";

function Drawer({ call }: LayerComponentProps<void, void>) {
  const group = useLayerGroup(call);
  return (
    <div role="dialog">
      <button onClick={() => void group.open({ ...nestedConfirm, payload })}>
        Edit details
      </button>
      <group.Outlet />
    </div>
  );
}
import { useLayerGroup } from "@stainless-code/solid-layers";

function Drawer(props: LayerComponentProps<void, void>) {
  const group = useLayerGroup(props.call);
  return (
    <div role="dialog">
      <button onClick={() => void group.open({ ...nestedConfirm, payload })}>
        Edit details
      </button>
      <group.Outlet />
    </div>
  );
}
import { useLayerGroup } from "@stainless-code/angular-layers";

@Component({/* … */})
class Drawer {
  group = useLayerGroup(this.call);
  openNested() {
    void this.group.open({ ...nestedConfirm, payload });
  }

  constructor() {
    const vcr = inject(ViewContainerRef);
    this.group.renderInto(vcr); // child stack renders here
  }
}
<script setup lang="ts">
import { useLayerGroup } from "@stainless-code/vue-layers";
import type { LayerComponentProps } from "@stainless-code/vue-layers";

const { call } = defineProps<LayerComponentProps<void, void>>();
const group = useLayerGroup(call);
</script>

<template>
  <div role="dialog">
    <button @click="() => void group.open({ ...nestedConfirm, payload })">
      Edit details
    </button>
    <group.Outlet />
  </div>
</template>
import { LitElement, html, type PropertyValues } from "lit";
import { property } from "lit/decorators.js";
import { useLayerGroup } from "@stainless-code/lit-layers";
import type { LayerComponentProps } from "@stainless-code/lit-layers";

class Drawer extends LitElement {
  @property({ attribute: false })
  declare call: LayerComponentProps<void, void>["call"];

  #group?: ReturnType<typeof useLayerGroup>;

  createRenderRoot() {
    return this; // light DOM — overlays stack where mounted
  }

  willUpdate(changed: PropertyValues<this>) {
    // `call` is set by StackOutlet after construct — bind once it arrives.
    if (changed.has("call") && this.call && !this.#group) {
      this.#group = useLayerGroup(this, this.call);
    }
  }

  render() {
    return html`<div role="dialog">
      <button
        type="button"
        @click=${() => void this.#group!.open({ ...nestedConfirm, payload })}
      >
        Edit details
      </button>
      ${this.#group?.outlet()}
    </div>`;
  }
}
<div role="dialog" x-data="drawer($layer.call)">
  <button type="button" @click="openNested()">Edit details</button>
  <template x-layer-outlet="stackId">
    <div role="dialog">
      <p x-text="$layer.payload.title"></p>
      <button type="button" @click="$layer.call.end(true)">Done</button>
    </div>
  </template>
</div>
import Alpine from "alpinejs";
import layers, {
  layerOptions,
  setLayerClient,
  useLayerGroup,
} from "@stainless-code/alpine-layers";

Alpine.plugin(layers);
setLayerClient();

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

Alpine.data("drawer", (call) => {
  const group = useLayerGroup(call);
  return {
    stackId: group.stackId,
    openNested() {
      void group.open({
        ...nestedConfirm,
        payload: { title: "Nested" },
      });
    },
    destroy() {
      group.dispose();
    },
  };
});

Nest x-layer-outlet on group.stackId inside a parent outlet row (x-data="drawer($layer.call)"). Alpine auto-calls destroy() on teardown; call dispose() yourself only when tearing down outside that lifecycle.

<script lang="ts">
  import { useLayerGroup } from "@stainless-code/svelte-layers";

  let { call } = $props();
  const group = useLayerGroup(call);</script>

<div role="dialog">
  <button onclick={() => void group.open({ ...nestedConfirm, payload })}>
    Edit details
  </button>
  {#each group.stack.current as state (state.id)}
    {@const nestedCall = group.stack.callFor(state)}
    {#if nestedCall}<NestedConfirm call={nestedCall} />{/if}
  {/each}
</div>
<script lang="ts">
  import { callFor, useLayerClient, useLayerGroup } from "@stainless-code/svelte-layers/store";

  let { call } = $props();
  const client = useLayerClient();
  const group = useLayerGroup(call);  const childStack = group.stack;
</script>

<div role="dialog">
  <button onclick={() => void group.open({ ...nestedConfirm, payload })}>
    Edit details
  </button>
  {#each $childStack as state (state.id)}
    {@const nestedCall = callFor(client, group.stackId, state)}
    {#if nestedCall}<NestedConfirm call={nestedCall} />{/if}
  {/each}
</div>

Store entry renders via $childStack + standalone callFor — same group.open call.

Inspect the group handle

Member Role
open Opens on the child stack (stack id pre-bound)
dismissAll Closes all child layers
states / stack Reactive snapshot of child layers
Outlet / outlet() / renderInto / x-layer-outlet / stack Render surface (adapter-specific)
stackId Derived id — `${parentStackId}~${parentLayerId}~${name}`

No second LayerClient, no manual cleanup. See Lifecycle for how dismissal propagates.

Last updated on July 19, 2026

Was this page helpful?