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.