diff --git a/.agents/skills/docs-voice/SKILL.md b/.agents/skills/docs-voice/SKILL.md
index 36adf09..282505d 100644
--- a/.agents/skills/docs-voice/SKILL.md
+++ b/.agents/skills/docs-voice/SKILL.md
@@ -34,7 +34,7 @@ was rejected) and the "when **not** to use it" anti-sell — keep both.
`meta.ts` icons and tab icons stay. Nested brand groups (e.g. Svelte): icon
on the group only — don't repeat the same SVG on indented children.
- **Adapter list order:** core/vanilla → react → preact → solid → angular →
- vue → svelte (runes → store). Every multi-adapter listing.
+ vue → lit → svelte (runes → store). Every multi-adapter listing.
## Don't
diff --git a/.agents/skills/harden-pr/LEDGER.md b/.agents/skills/harden-pr/LEDGER.md
index 894122c..72fab88 100644
--- a/.agents/skills/harden-pr/LEDGER.md
+++ b/.agents/skills/harden-pr/LEDGER.md
@@ -14,6 +14,9 @@ By-design or false-positive findings — do not re-raise.
- **[correctness]** `packages/core/src/layerStack.ts` — snapshot cached + rebuilt inside `notifyManager.batch`: by-design — `useSyncExternalStore` needs a referentially-stable snapshot between mutations or it infinite-loops.
-->
+- **[public-api]** `packages/lit/src/index.ts` `layerClientContext` export — by-design; rationale on the const JSDoc.
+- **[public-api]** `packages/lit/src/index.ts` `StackController` `queued`/`deferClient`/`bindClient` — by-design; rationale on the class/ctor JSDoc (factories are the supported path).
+
## Deferred
Capped or out-of-scope-for-now — reconcile re-vets; remove lines when fixed.
diff --git a/.agents/skills/improve-codebase-architecture/LANGUAGE.md b/.agents/skills/improve-codebase-architecture/LANGUAGE.md
index 0eff3e2..3a7354c 100644
--- a/.agents/skills/improve-codebase-architecture/LANGUAGE.md
+++ b/.agents/skills/improve-codebase-architecture/LANGUAGE.md
@@ -31,7 +31,7 @@ _Examples in this repo_: the package seam in `docs/architecture.md` — **core v
**Adapter**
A concrete thing that satisfies an interface at a seam. Describes _role_ (what slot it fills), not substance (what's inside).
-_Examples in this repo_: each framework binding (`react`, `svelte`, `vue`, `solid`, `preact`, `angular`) is an adapter at the core/adapter seam. Six adapters across the seam = a real seam (the engine is genuinely framework-agnostic); a single adapter would be hypothetical.
+_Examples in this repo_: each framework binding (`react`, `preact`, `solid`, `angular`, `vue`, `lit`, `svelte`) is an adapter at the core/adapter seam. Seven adapters across the seam = a real seam (the engine is genuinely framework-agnostic); a single adapter would be hypothetical.
**Leverage**
What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
@@ -51,7 +51,7 @@ _Example_: the `notifyManager.batch` wrapping lives in one place in `packages/co
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test _past_ the interface, the module is probably the wrong shape. Example: the zero-dep core invariant (`packages/core` has no dependencies and imports no framework peer) belongs at the package seam, enforced by package manifests plus dependency/import tooling rather than tests that mock internals.
-- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. Example: a core/adapter seam with only a React adapter is hypothetical; add Svelte, Vue, Solid, Preact, and Angular and it's a real seam (each framework's reactivity is genuinely different). The seam is justified because the engine is the same across all six — don't introduce a per-adapter core fork before a second adapter genuinely needs it.
+- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. Example: a core/adapter seam with only a React adapter is hypothetical; add Preact, Solid, Angular, Vue, Lit, and Svelte and it's a real seam (each framework's reactivity is genuinely different). The seam is justified because the engine is the same across all seven — don't introduce a per-adapter core fork before a second adapter genuinely needs it.
## Relationships
diff --git a/.changeset/lit-adapter.md b/.changeset/lit-adapter.md
new file mode 100644
index 0000000..3c33f43
--- /dev/null
+++ b/.changeset/lit-adapter.md
@@ -0,0 +1,5 @@
+---
+"@stainless-code/lit-layers": minor
+---
+
+Add `@stainless-code/lit-layers` — full-matrix Lit adapter (Reactive Controllers, `@lit/context`, light-DOM stack CEs via `defineStackElements()`).
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index a1d6a84..cef4f9a 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -3,7 +3,7 @@
`@stainless-code/layers` is a small, freshly extracted library. Before large PRs, please open an issue so we can align on:
- **Public surface** — anything exported from a package entry point (`src/index.ts` / React's `src/index.tsx`, plus Svelte's `src/store.ts`) is the public API and must carry JSDoc that reads well in hovers and published typings. See [`docs/architecture.md`](../docs/architecture.md) for the core + adapter model.
-- **Runtimes** — **Node** `^20.19.0 || >=22.12.0` and **Bun** `>=1.0.0` (`package.json` **engines**). The core is zero-dep by design; each adapter package declares its required peer (`react`, `preact`, `svelte`, `vue`, `solid-js`, `@angular/core`).
+- **Runtimes** — **Node** `^20.19.0 || >=22.12.0` and **Bun** `>=1.0.0` (`package.json` **engines**). The core is zero-dep by design; each adapter package declares its required peer (`react`, `preact`, `solid-js`, `@angular/core`, `vue`, `lit` + `@lit/context`, `svelte`).
## Dev workflow
diff --git a/.github/ISSUE_TEMPLATE/1_bug.yml b/.github/ISSUE_TEMPLATE/1_bug.yml
index ec581ca..0c2e144 100644
--- a/.github/ISSUE_TEMPLATE/1_bug.yml
+++ b/.github/ISSUE_TEMPLATE/1_bug.yml
@@ -49,7 +49,7 @@ body:
- Node.js
- Bun
- Browser (React)
- - Browser (Svelte / Vue / Solid / Preact / Angular)
+ - Browser (Svelte / Vue / Solid / Preact / Angular / Lit)
- Both / not sure
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/2_feature_adapter.yml b/.github/ISSUE_TEMPLATE/2_feature_adapter.yml
index f4f26f2..a171c6e 100644
--- a/.github/ISSUE_TEMPLATE/2_feature_adapter.yml
+++ b/.github/ISSUE_TEMPLATE/2_feature_adapter.yml
@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
- **Adapters** plug in via one seam — a binding that subscribes to `LayerStack.subscribe`/`getSnapshot` and renders the active stack in that library or framework's reactivity (`useSyncExternalStore`, Svelte runes, Vue refs, Solid accessors, Angular signals, Preact compat). See [`docs/architecture.md`](https://github.com/stainless-code/layers/blob/main/docs/architecture.md). Open an issue here first so we can agree on shape (entry point, optional peer, API) before a large PR.
+ **Adapters** plug in via one seam — a binding that subscribes to `LayerStack.subscribe`/`getSnapshot` and renders the active stack in that library or framework's reactivity (`useSyncExternalStore`, Svelte runes, Vue refs, Solid accessors, Angular signals, Preact compat, Lit Reactive Controllers). See [`docs/architecture.md`](https://github.com/stainless-code/layers/blob/main/docs/architecture.md). Open an issue here first so we can agree on shape (entry point, optional peer, API) before a large PR.
- type: textarea
id: problem
diff --git a/.size-limit.json b/.size-limit.json
index 8ddfa2e..445746e 100644
--- a/.size-limit.json
+++ b/.size-limit.json
@@ -45,6 +45,13 @@
"limit": "3.5 KB",
"gzip": true
},
+ {
+ "name": "@stainless-code/lit-layers",
+ "path": "packages/lit/dist/index.mjs",
+ "ignore": ["@stainless-code/layers", "lit", "@lit/context"],
+ "limit": "4.5 KB",
+ "gzip": true
+ },
{
"name": "@stainless-code/solid-layers",
"path": "packages/solid/dist/index.mjs",
diff --git a/README.md b/README.md
index 293c73a..8365605 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
Modals are just async functions you forgot to `await`.
-Headless layer/stack manager — modals, dialogs, drawers, popovers, toasts are **layers in named stacks**. Open any layer from anywhere and `await` a typed result. A zero-dependency core plus React, Preact, Solid, Angular, Vue, and Svelte adapters. State coordination, not UI ownership: Layers owns ordering, keys, transitions, blockers, and the `await client.open(...)` contract; you own rendering, focus, portals, and a11y.
+Headless layer/stack manager — modals, dialogs, drawers, popovers, toasts are **layers in named stacks**. Open any layer from anywhere and `await` a typed result. A zero-dependency core plus React, Preact, Solid, Angular, Vue, Lit, and Svelte adapters. State coordination, not UI ownership: Layers owns ordering, keys, transitions, blockers, and the `await client.open(...)` contract; you own rendering, focus, portals, and a11y.
> Experimental — the API may change between minor releases. Pin your version. ([Stability & versioning](https://stainless-code.com/layers/concepts/stability))
@@ -15,15 +15,16 @@ Full docs: [stainless-code.com/layers](https://stainless-code.com/layers).
## Packages
-| Package | Peer | Install |
-| ---------------------------------------------------- | --------------- | ---------------------------------------- |
-| [`@stainless-code/layers`](packages/core) | — | `bun add @stainless-code/layers` |
-| [`@stainless-code/react-layers`](packages/react) | `react` | `bun add @stainless-code/react-layers` |
-| [`@stainless-code/preact-layers`](packages/preact) | `preact` | `bun add @stainless-code/preact-layers` |
-| [`@stainless-code/solid-layers`](packages/solid) | `solid-js` | `bun add @stainless-code/solid-layers` |
-| [`@stainless-code/angular-layers`](packages/angular) | `@angular/core` | `bun add @stainless-code/angular-layers` |
-| [`@stainless-code/vue-layers`](packages/vue) | `vue` | `bun add @stainless-code/vue-layers` |
-| [`@stainless-code/svelte-layers`](packages/svelte) | `svelte` | `bun add @stainless-code/svelte-layers` |
+| Package | Peer | Install |
+| ---------------------------------------------------- | --------------------- | ---------------------------------------- |
+| [`@stainless-code/layers`](packages/core) | — | `bun add @stainless-code/layers` |
+| [`@stainless-code/react-layers`](packages/react) | `react` | `bun add @stainless-code/react-layers` |
+| [`@stainless-code/preact-layers`](packages/preact) | `preact` | `bun add @stainless-code/preact-layers` |
+| [`@stainless-code/solid-layers`](packages/solid) | `solid-js` | `bun add @stainless-code/solid-layers` |
+| [`@stainless-code/angular-layers`](packages/angular) | `@angular/core` | `bun add @stainless-code/angular-layers` |
+| [`@stainless-code/vue-layers`](packages/vue) | `vue` | `bun add @stainless-code/vue-layers` |
+| [`@stainless-code/lit-layers`](packages/lit) | `lit`, `@lit/context` | `bun add @stainless-code/lit-layers` |
+| [`@stainless-code/svelte-layers`](packages/svelte) | `svelte` | `bun add @stainless-code/svelte-layers` |
Install only the adapter for your framework — it pulls the core in transitively and re-exports it, so adapter APIs (`StackProvider`, `useStack`, …) and core APIs (`LayerClient`, `layerOptions`, …) both come from the one package. Each adapter lists its framework as a required peer. Svelte ships two entries: `@stainless-code/svelte-layers` (runes, 5.7+) and `@stainless-code/svelte-layers/store` (stores, 3+).
diff --git a/apps/docs/blume.config.ts b/apps/docs/blume.config.ts
index 10d6bcf..e4b66b4 100644
--- a/apps/docs/blume.config.ts
+++ b/apps/docs/blume.config.ts
@@ -3,7 +3,7 @@ import { defineConfig } from "blume";
export default defineConfig({
title: "Layers",
description:
- "Headless modal/dialog/drawer/popover/toast manager — open any layer from anywhere. Zero-dep core + React, Preact, Solid, Angular, Vue, and Svelte adapters.",
+ "Headless modal/dialog/drawer/popover/toast manager — open any layer from anywhere. Zero-dep core + React, Preact, Solid, Angular, Vue, Lit, and Svelte adapters.",
logo: { image: "/logo.svg", text: "Layers" },
diff --git a/apps/docs/components/blume/Footer.astro b/apps/docs/components/blume/Footer.astro
index 3f7fb67..db6bb97 100644
--- a/apps/docs/components/blume/Footer.astro
+++ b/apps/docs/components/blume/Footer.astro
@@ -47,6 +47,7 @@ const groups: { label: string; links: { href: string; label: string; external?:
{ href: contentHref("/adapters/solid"), label: "Solid" },
{ href: contentHref("/adapters/angular"), label: "Angular" },
{ href: contentHref("/adapters/vue"), label: "Vue" },
+ { href: contentHref("/adapters/lit"), label: "Lit" },
{ href: contentHref("/adapters/svelte/runes"), label: "Svelte" },
],
},
@@ -153,7 +154,8 @@ const groups: { label: string; links: { href: string; label: string; external?:
class="inline-block size-1.5 rounded-full bg-accent"
aria-hidden="true"
/>
- Experimental — the API may change between minor releases.
+ Experimental — the API may change between minor releases. Pin your
+ version.
`;
+ }
+}
+
+export const confirm = layerOptions({
+ stack: "confirm",
+ key: ["confirm", "remove"],
+ component: ConfirmDialog,
+});
+```
+
+```ts
+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``;
+ }
+}
+
+@customElement("app-shell")
+class AppShell extends LitElement {
+ constructor() {
+ super();
+ provideLayerClient(this);
+ }
+
+ render() {
+ return html`
+ `;
+ }
+}
+```
+
+## 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` — `.current` of the selected stack slice |
+| `useQueuedStack(host, { ... }, client?)` | `StackController` 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` | `` (`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
+
+- [Lifecycle](/concepts/lifecycle) · [Identity & types](/concepts/identity-and-types)
+- [Adapter parity](/adapters)
diff --git a/apps/docs/content/adapters/meta.ts b/apps/docs/content/adapters/meta.ts
index 97d3f36..7183c12 100644
--- a/apps/docs/content/adapters/meta.ts
+++ b/apps/docs/content/adapters/meta.ts
@@ -3,5 +3,5 @@ import { defineMeta } from "blume";
export default defineMeta({
title: "Adapters",
icon: "plug",
- pages: ["core", "react", "preact", "solid", "angular", "vue", "svelte"],
+ pages: ["core", "react", "preact", "solid", "angular", "vue", "lit", "svelte"],
});
diff --git a/apps/docs/content/concepts/comparison.mdx b/apps/docs/content/concepts/comparison.mdx
index 4c32eb9..908ba8f 100644
--- a/apps/docs/content/concepts/comparison.mdx
+++ b/apps/docs/content/concepts/comparison.mdx
@@ -27,7 +27,7 @@ Two views: this matrix contrasts **one exemplar per category**; the [full spectr
| Capability | Layers | react-call | nice-modal | overlay-kit | @mantine/modals | overlastic |
| --- | :---: | :---: | :---: | :---: | :---: | :---: |
| **Category** | Orchestration engine | Orchestration engine | Registry / reducer | Registry / reducer | Batteries-included UI | Orchestration engine |
-| Multi-framework | ✅ 6 | ❌ R | ❌ R | ❌ R | ❌ R | ✅ 4 |
+| Multi-framework | ✅ 7 | ❌ R | ❌ R | ❌ R | ❌ R | ✅ 4 |
| Zero-dep core | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Typed await | ✅ DataTag | ✅ | 🟡 | 🟡 | ❌ | 🟡 |
| Named stacks | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
@@ -48,13 +48,13 @@ Focus containment, ARIA/dialog semantics, portals/top-layer, backdrop/Escape, sc
## Across the whole landscape
-The matrix above contrasts one exemplar per category. The tables below are the full spectrum — every audited library a real evaluator weighs, across all six frameworks, scored on the engine axes Layers targets. One table per category (orchestration engines split into multi-framework and single-framework so no table is wider than the eye can scan). **Layers is the baseline column in every table — the only column green on every row.** `R/V/S/Solid/Ang` = React/Vue/Svelte/Solid/Angular; `Nfw` = N frameworks.
+The matrix above contrasts one exemplar per category. The tables below are the full spectrum — every audited library a real evaluator weighs, across all six frameworks, scored on the engine axes Layers targets. One table per category (orchestration engines split into multi-framework and single-framework so no table is wider than the eye can scan). **Layers is the baseline column in every table — the only column green on every row.** `R/V/S/Solid/Ang/Lit` = React/Vue/Svelte/Solid/Angular/Lit; `Nfw` = N frameworks.
**Multi-framework orchestration engines**
| Capability | Layers | overlastic | ui-call | dialog-fn | @gwn-sheet-stack |
| --- | :---: | :---: | :---: | :---: | :---: |
-| Multi-framework | ✅ 6 | ✅ 4 | ✅ 5 | 🟡 3 | 🟡 2 |
+| Multi-framework | ✅ 7 | ✅ 4 | ✅ 5 | 🟡 3 | 🟡 2 |
| Zero-dep core | ✅ | ✅ core | ✅ | ✅ | ✅ |
| Typed await | ✅ DataTag | 🟡 generic | 🟡 generic | 🟡 dismiss=undefined | ✅ `push` |
| Named stacks | ✅ | ❌ | ❌ | ❌ | ❌ |
@@ -69,7 +69,7 @@ The matrix above contrasts one exemplar per category. The tables below are the f
| Capability | Layers | react-call | jenesius-vue-modal | ngx-call | @angular-libs/dialog | @svelte-put/async-stack |
| --- | :---: | :---: | :---: | :---: | :---: | :---: |
-| Multi-framework | ✅ 6 | ❌ React | ❌ Vue | ❌ Angular | ❌ Angular | ❌ Svelte |
+| Multi-framework | ✅ 7 | ❌ React | ❌ Vue | ❌ Angular | ❌ Angular | ❌ Svelte |
| Zero-dep core | ✅ | ✅ | — | ✅ | — | ✅ |
| Typed await | ✅ DataTag | ✅ generic | 🟡 any | ✅ generic | ❌ | ✅ resolution |
| Named stacks | ✅ | ❌ | ✅ NamespaceStore | ❌ | ❌ global list | ❌ single |
@@ -84,7 +84,7 @@ The matrix above contrasts one exemplar per category. The tables below are the f
| Capability | Layers | nice-modal-react | overlay-kit | pushmodal | @ngneat/dialog |
| --- | :---: | :---: | :---: | :---: | :---: |
-| Multi-framework | ✅ 6 | ❌ React | ❌ React | ❌ React | ❌ Angular |
+| Multi-framework | ✅ 7 | ❌ React | ❌ React | ❌ React | ❌ Angular |
| Zero-dep core | ✅ | ✅ | ✅ | — | — |
| Typed await | ✅ DataTag | 🟡 unknown | 🟡 generic | ❌ void | ✅ afterClosed$ |
| Named stacks | ✅ | ❌ id-map | ❌ | ✅ LIFO | ❌ |
@@ -99,7 +99,7 @@ The matrix above contrasts one exemplar per category. The tables below are the f
| Capability | Layers | @mantine/modals | vue-final-modal | svelte-promise-modals | kolirt/vue-modal |
| --- | :---: | :---: | :---: | :---: | :---: |
-| Multi-framework | ✅ 6 | ❌ React | ❌ Vue | ❌ Svelte | ❌ Vue |
+| Multi-framework | ✅ 7 | ❌ React | ❌ Vue | ❌ Svelte | ❌ Vue |
| Zero-dep core | ✅ | ❌ @mantine/core | ❌ @vueuse+focus-trap | ❌ focus-trap+deepmerge | — |
| Typed await | ✅ DataTag | ❌ string id | ❌ lifecycle strings | ✅ closeModal prop | ✅ awaitable |
| Named stacks | ✅ | ❌ | ❌ | ❌ | ✅ grouped |
@@ -114,7 +114,7 @@ The matrix above contrasts one exemplar per category. The tables below are the f
| Capability | Layers | @corvu/dialog | Ark / Zag |
| --- | :---: | :---: | :---: |
-| Multi-framework | ✅ 6 | ❌ Solid | ✅ R/V/S/Solid |
+| Multi-framework | ✅ 7 | ❌ Solid | ✅ R/V/S/Solid |
| Zero-dep core | ✅ | ✅ | ✅ |
| Typed await | ✅ DataTag | ❌ no await | ❌ no await |
| Named stacks | ✅ | ❌ | ❌ |
diff --git a/apps/docs/content/concepts/glossary.mdx b/apps/docs/content/concepts/glossary.mdx
index 97436c5..5ab71f1 100644
--- a/apps/docs/content/concepts/glossary.mdx
+++ b/apps/docs/content/concepts/glossary.mdx
@@ -48,16 +48,16 @@ From `LayerClient` down to `keySignature` — every term in code, docs, and issu
- **createLayer** — core factory: `options` + `client` → headless `LayerHandle` / `ValidatedLayerHandle` (layer ops + `stack`/`client`/`options`/`current` escapes; no reactive field).
- **LayerHandle** — return of plain `createLayer`. `open`/`upsert` take payload only; `dismiss` takes `{ id?, force? }`; `update` / `cancelQueued` take `{ id? }` (see **cancelQueued**).
- **ValidatedLayerHandle** — when `options.validate` is set: `open`/`upsert` = schema **input**; `current`/`update` = parsed **output**. Wired adapters also expose reactive `state`/`queued`/`top` as output.
- - **useLayer** — adapter wired handle (`createLayer` + reactive `state`/`queued`/`top`). Svelte: `createLayer`; Angular: `injectLayer`.
+ - **useLayer** — adapter wired handle (`createLayer` + reactive `state`/`queued`/`top`). Lit: `useLayer(host, options, client?)`. Svelte: `createLayer`; Angular: `injectLayer`.
- **useLayerState** / **useLayerQueuedState** / **useQueuedStack** — observe-only (mounted per-key / queued per-key / queued whole-stack); return `LayerState[]` (or framework wrappers). Renamed/added vs the old singular `useLayer(key, …)`.
- **current** — live-checked bound instance on a handle (`Layer | null`); correlation + explicit `{ id }` — not a hidden control default.
- **Drive vs observe** — **drive** = wired handle (`useLayer` / `injectLayer` / `createLayer`) for `open`/`upsert`/`dismiss`/`update`/`cancelQueued`; **observe** = `useLayerState` / `useLayerQueuedState` / `useQueuedStack` / `useStack` for reactive snapshots without control. Both paths are first-class; `client.open({ …layerOptions, payload })` remains valid.
- - **Observer / selector** — adapters subscribe to `LayerStack.getSnapshot()` and project via a selector (React/Preact `useSyncExternalStore`, Solid `from`, Angular `signal`, Vue `shallowRef`, Svelte runes).
+ - **Observer / selector** — adapters subscribe to `LayerStack.getSnapshot()` and project via a selector (React/Preact `useSyncExternalStore`, Solid `from`, Angular `signal`, Vue `shallowRef`, Lit reactive controllers, Svelte runes).
- **Options helper** (`layerOptions`) — identity function carrying `
` generics so `LayerClient.open` infers the response type end-to-end.
- - **Outlet** — renders the active stack. React, Preact, and Solid ship `StackOutlet`; Angular exposes `renderStack(vcr)`; Vue ships `StackOutlet`; Svelte renders from `useStack().current` + `callFor`. See [adapter ergonomics](/adapters#adapter-ergonomics).
- - **Stack handles** (`useStackHandles`) — headless `{ states, getCall }` for custom hosts (React/Preact/Solid/Angular/Vue; Svelte's `useStack()` already returns `.current` + `callFor`). `StackOutlet` is the registered-component convenience built on it where shipped.
+ - **Outlet** — renders the active stack. React, Preact, Solid, and Vue ship `StackOutlet`; Angular exposes `renderStack(vcr)`; Lit ships ``; Svelte renders from `useStack().current` + `callFor`. See [adapter ergonomics](/adapters#adapter-ergonomics).
+ - **Stack handles** (`useStackHandles`) — headless `{ states, getCall }` for custom hosts (React/Preact/Solid/Angular/Vue/Lit; Svelte's `useStack()` already returns `.current` + `callFor`). `StackOutlet` is the registered-component convenience built on it where shipped.
- **Validator** (`validate`) — a Standard Schema or sync `(input) => output` fn that parses/validates a layer's `payload` at `open`; the layer stores the parsed output. Failure rejects `open` with a `PayloadValidationError`.
diff --git a/apps/docs/content/concepts/overview.mdx b/apps/docs/content/concepts/overview.mdx
index 542cc25..7e89e5c 100644
--- a/apps/docs/content/concepts/overview.mdx
+++ b/apps/docs/content/concepts/overview.mdx
@@ -1,6 +1,6 @@
---
title: Overview
-description: Overlay state is cross-cutting; a zero-dep headless engine plus six framework adapters split it from UI rendering.
+description: Overlay state is cross-cutting; a zero-dep headless engine plus seven framework adapters split it from UI rendering.
search:
tags: ["concept"]
---
diff --git a/apps/docs/content/concepts/stability.mdx b/apps/docs/content/concepts/stability.mdx
index 4959991..909f10a 100644
--- a/apps/docs/content/concepts/stability.mdx
+++ b/apps/docs/content/concepts/stability.mdx
@@ -11,7 +11,7 @@ While the project is pre-1.0, treat semver as a signal, not a guarantee of zero
## What we ship
-- A **zero-dep core** (`@stainless-code/layers`) and **six framework adapters** with engine + primitive parity — React, Preact, Solid, Angular, Vue, and Svelte.
+- A **zero-dep core** (`@stainless-code/layers`) and **seven framework adapters** with engine + primitive parity — React, Preact, Solid, Angular, Vue, Lit, and Svelte.
- When the engine gains capability, every adapter moves together.
- Angular and Svelte diverge by design at the rendering surface (compiler-free builds → imperative / primitive APIs, not shipped outlet components).
diff --git a/apps/docs/content/concepts/when-to-use.mdx b/apps/docs/content/concepts/when-to-use.mdx
index f57d8a3..1e406e9 100644
--- a/apps/docs/content/concepts/when-to-use.mdx
+++ b/apps/docs/content/concepts/when-to-use.mdx
@@ -19,7 +19,7 @@ Awaiting a result is **optional** — `void client.open({ ...toast, payload })`
| Enter/exit animated overlays | `transition` + `enteringDelay`/`exitingDelay` `settle()` | Ideal |
| Guard dismissal ("discard unsaved changes?") | blockers (`addBlocker`, `dismissing`, `dismissAll`) | Ideal |
| Validate an untrusted payload at open | `validate` (Standard Schema or a function) | Nice-to-have |
-| One overlay system across React/Preact/Solid/Angular/Vue/Svelte | UI-agnostic core + adapters | Ideal |
+| One overlay system across React/Preact/Solid/Angular/Vue/Lit/Svelte | UI-agnostic core + adapters | Ideal |
| A single, always-local overlay opened from one component — no return, stacking, queue, animation, or guard, and no wish for a global registry | — | Skip → plain controlled component |
| Static/inline content with no overlay semantics; full-page navigation | — | Skip → plain markup / a router |
diff --git a/apps/docs/content/examples/confirm-dialog.mdx b/apps/docs/content/examples/confirm-dialog.mdx
index c18d451..b832cef 100644
--- a/apps/docs/content/examples/confirm-dialog.mdx
+++ b/apps/docs/content/examples/confirm-dialog.mdx
@@ -14,6 +14,7 @@ import confirmSolidSrc from "../../recipes/confirm-dialog/solid.tsx?raw";
import confirmSvelteRunesSrc from "../../recipes/confirm-dialog/svelte-runes.svelte?raw";
import confirmSvelteStoreSrc from "../../recipes/confirm-dialog/svelte-store.svelte?raw";
import confirmAngularSrc from "../../recipes/confirm-dialog/angular.ts?raw";
+import confirmLitSrc from "../../recipes/confirm-dialog/lit.ts?raw";
A delete button in a sidebar needs a yes/no. `useLayer(confirm)` + `await confirm.open({ title })` returns the typed boolean.
@@ -41,6 +42,7 @@ One complete recipe per framework adapter (live demo above is React).
+
diff --git a/apps/docs/content/guides/headless-rendering.mdx b/apps/docs/content/guides/headless-rendering.mdx
index 3861ce6..8325a0f 100644
--- a/apps/docs/content/guides/headless-rendering.mdx
+++ b/apps/docs/content/guides/headless-rendering.mdx
@@ -7,7 +7,7 @@ search:
`StackOutlet` maps a stack snapshot to registered components — the convenience path. When you need custom portals, per-layer switches, or non-component renderers, use the headless primitives.
-## useStackHandles
+## Map stacks with useStackHandles
Returns `{ states, getCall }` — everything `StackOutlet` uses internally:
@@ -136,6 +136,32 @@ const { states, getCall } = useStackHandles("modal");
```
+
+
+
+```ts lineNumbers
+import { LitElement, html } from "lit";
+import { useStackHandles } from "@stainless-code/lit-layers";
+
+class CustomHost extends LitElement {
+ #handles = useStackHandles(this, "modal");
+
+ createRenderRoot() {
+ return this;
+ }
+
+ render() {
+ return html`${this.#handles.states.current.map(
+ (s) => html``,
+ )}`;
+ }
+}
+```
+
@@ -179,7 +205,7 @@ Store entry subscribes via `$stack` + standalone `callFor(client, stackId, state
`StackOutlet` deliberately has **no** `render` prop — headless is the escape hatch.
-## StackSubscribe
+## Subscribe with StackSubscribe
Isolate a selector in JSX/template without pulling the full snapshot into your component:
@@ -262,6 +288,30 @@ import { StackSubscribe } from "@stainless-code/vue-layers";
```
+
+
+
+```ts lineNumbers
+import { LitElement, html } from "lit";
+import { defineStackElements } from "@stainless-code/lit-layers";
+
+defineStackElements();
+
+class OpenCount extends LitElement {
+ createRenderRoot() {
+ return this;
+ }
+
+ render() {
+ return html` s.length}
+ .renderer=${(count) => html`${count} open`}
+ >`;
+ }
+}
+```
+
@@ -302,8 +352,10 @@ For fully-typed selector output in Vue, prefer `useStack({ stack, select })` in
| Adapter | Headless path | Built-in outlet |
| -------------- | -------------------------------------------- | ------------------------- |
| React / Preact | `useStackHandles`, `StackSubscribe` | `StackOutlet` |
-| Solid / Vue | `useStackHandles`, `StackSubscribe` | `StackOutlet` |
+| Solid | `useStackHandles`, `StackSubscribe` | `StackOutlet` |
| Angular | `useStackHandles`, `renderStack(vcr)` | `renderStack` |
+| Vue | `useStackHandles`, `StackSubscribe` | `StackOutlet` |
+| Lit | `useStackHandles`, `` | `` |
| Svelte (runes) | `useStack().current` + `callFor` | — (render in your markup) |
| Svelte (store) | `$stack` + `callFor(client, stackId, state)` | — |
diff --git a/apps/docs/content/guides/install.mdx b/apps/docs/content/guides/install.mdx
index 5308833..be542f9 100644
--- a/apps/docs/content/guides/install.mdx
+++ b/apps/docs/content/guides/install.mdx
@@ -55,6 +55,14 @@ Peer: `vue` (≥3.3)
npm i @stainless-code/vue-layers
```
+## Lit
+
+Peers: `lit` (≥3.2), `@lit/context` (≥1.1)
+
+```package-install
+npm i @stainless-code/lit-layers
+```
+
## Svelte
Peer: `svelte` (≥3). Two package entries in one npm package:
diff --git a/apps/docs/content/guides/nested-overlays.mdx b/apps/docs/content/guides/nested-overlays.mdx
index a3628ca..6033722 100644
--- a/apps/docs/content/guides/nested-overlays.mdx
+++ b/apps/docs/content/guides/nested-overlays.mdx
@@ -7,9 +7,9 @@ search:
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 drain automatically.
-## Core primitive
+## 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)`:
+`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, …)`):
```ts lineNumbers
import { createLayerGroup } from "@stainless-code/layers";
@@ -20,9 +20,9 @@ group.dismissAll();
group.dispose(); // usually automatic via adapter lifecycle
```
-## Inline child outlet
+## Render the child stack inline
-Render the child stack inside the parent layer's DOM. React, Preact, Solid, and Vue ship an `Outlet` component; Angular uses `renderInto`; Svelte exposes the child `stack` directly:
+Render the child stack inside the parent layer's DOM. React, Preact, Solid, Angular, Vue, Lit, and Svelte each expose a child render path — `Outlet`, `renderInto`, `outlet()`, or the child `stack` (tabs below):
@@ -123,6 +123,46 @@ const group = useLayerGroup(call);
```
+
+
+
+```ts lineNumbers
+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["call"];
+
+ #group?: ReturnType;
+
+ createRenderRoot() {
+ return this; // light DOM — overlays stack where mounted
+ }
+
+ willUpdate(changed: PropertyValues) {
+ // `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`
- One core, six adapters.
+ One core, seven adapters.
Declare a layer once, then open and
- Every adapter exposes the same primitives and re-exports the full core.
- Angular and Svelte diverge by design — imperative render instead of
- shipped outlet components.
+ React, Preact, Solid, Angular, Vue, and Lit re-export the full core;
+ Svelte re-exports selectively. Angular and Svelte diverge by design —
+ imperative render instead of shipped outlet components.
-
+
Capability
diff --git a/apps/docs/pages/_home/Hero.astro b/apps/docs/pages/_home/Hero.astro
index d7a2842..0793cd1 100644
--- a/apps/docs/pages/_home/Hero.astro
+++ b/apps/docs/pages/_home/Hero.astro
@@ -20,7 +20,7 @@ const defaultFramework = "vanilla";
class="inline-flex items-center rounded-full border border-border px-3 py-1 text-muted-foreground text-xs transition-colors hover:border-foreground hover:text-foreground"
href={contentHref("/concepts/stability")}
>
- Experimental — the API may change between minor releases
+ Experimental — the API may change between minor releases. Pin your version.
- 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, six framework adapters.
+ 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, seven framework adapters.
- Layers is experimental — the API may change between minor releases. Pin
- your dependency version and read release notes before upgrading.
+ Experimental — the API may change between minor releases. Pin your
+ version.