Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

Lit

Lit adapter — provideLayerClient, Reactive Controllers, StackOutlet, custom elements.

@stainless-code/lit-layers binds stacks via Reactive Controllers + requestUpdate. The core is re-exported.

Install

npm install @stainless-code/lit-layers
pnpm add @stainless-code/lit-layers
yarn add @stainless-code/lit-layers
bun add @stainless-code/lit-layers

Peers: lit (>=3.2.0), @lit/context (>=1.1.0)

Client

Call provideLayerClient(host, client?) on a host (creates a LayerClient when omitted), or mount <stack-provider .client=${client}> high in the tree. Descendants omit client, or read useLayerClient(host).current:

import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { defineStackElements, provideLayerClient } from "@stainless-code/lit-layers";

defineStackElements();

@customElement("my-shell")
class MyShell extends LitElement {
  constructor() {
    super();
    provideLayerClient(this);
  }

  render() {
    return html`<slot></slot><stack-outlet stack="confirm"></stack-outlet>`;
  }
}

Render a stack

<stack-provider> is a Lit-idiomatic shadow root + <slot> (context still reaches light children). <stack-outlet> and <stack-subscribe> render in light DOM so overlays stack inline where mounted.

<stack-outlet> (the StackOutlet CE from defineStackElements()) maps registered component entries — a LitElement constructor or (props) => TemplateResult (no tag strings in v1). Id-keyed repeat keeps instances stable. Headless: useStackHandles(this, stack, rootProps).

<stack-subscribe .selector .renderer> takes a (value) => TemplateResult callback.

Confirm dialog

import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
  layerOptions,
  type LayerCallContext,
} from "@stainless-code/lit-layers";

type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;

@customElement("confirm-dialog")
class ConfirmDialog extends LitElement {
  // Light DOM (Lit: return `this` from createRenderRoot).
  createRenderRoot() {
    return this;
  }

  // Declared reactive — StackOutlet assigns these via LayerElementDirective.
  @property({ attribute: false })
  declare call: LayerCallContext<ConfirmPayload, ConfirmResponse>;

  @property({ attribute: false })
  declare payload: ConfirmPayload;

  render() {
    return html`<div role="dialog">
      <h2>${this.payload.title}</h2>
      <button type="button" @click=${() => void this.call.end(true)}>
        Yes
      </button>
      <button type="button" @click=${() => void this.call.end(false)}>
        No
      </button>
    </div>`;
  }
}

export const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import {
  defineStackElements,
  provideLayerClient,
  useLayer,
} from "@stainless-code/lit-layers";
import { confirm } from "./confirm-dialog";

defineStackElements();

@customElement("remove-button")
class RemoveButton extends LitElement {
  #confirm = useLayer(this, confirm);

  render() {
    return html`<button
      type="button"
      @click=${() => void this.#confirm.open({ title: "Remove item?" })}
    >
      Remove
    </button>`;
  }
}

@customElement("app-shell")
class AppShell extends LitElement {
  constructor() {
    super();
    provideLayerClient(this);
  }

  render() {
    return html`<remove-button></remove-button>
      <stack-outlet stack="confirm"></stack-outlet>`;
  }
}

Primitives

Options bag plus optional trailing LayerClient. Drive with useLayer(this, options, client?); observe with useLayerState(this, { key, … }).

Export Role
useStack(host, { stack?, select?, compare? }, client?) StackController<T>.current of the selected stack slice
useQueuedStack(host, { ... }, client?) StackController<T> over the queued snapshot
useLayer(host, options, client?) Drive — wired handle + reactive state / queued / top
useLayerState(host, { key, stack?, select?, compare? }, client?) Observe — mounted same-key layers
useLayerQueuedState(host, { key, ... }, client?) Observe — queued same-key layers

Ergonomic APIs

Export Role
useStackHandles(host, stack?, rootProps?, client?) { states, getCall } for headless rendering
StackSubscribe <stack-subscribe .selector .renderer> (renderer(value)TemplateResult)
useMutationFlow(host, call) pending: boolean; run(fn).orEnd(response)
useLayerGroup(host, call, options?, client?) Child stack with outlet(): TemplateResult + stackId
createStackHook(config?) StackProvider, useAppStack(host), AppHost CE, AppLayer controller

defineStackElements() idempotently registers stack-provider, stack-outlet, stack-subscribe, and app-host; not auto-invoked on import.

Learn more

Last updated on July 17, 2026

Was this page helpful?