vue-layers
Vue adapter for @stainless-code/vue-layers; open UI from anywhere and manage ordered, named stacks with typed or fire-and-forget `open`
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Vue adapter for @stainless-code/vue-layers; open UI from anywhere and manage ordered, named stacks with typed or fire-and-forget `open`
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Alpine.js adapter for @stainless-code/alpine-layers; plugin magics, x-layer-outlet, layerStack data, and JS hooks for typed layer stacks
Angular adapter for @stainless-code/angular-layers (signals + DI + imperative renderStack); use when wiring provideLayerClient, renderStack/useStackHandles, useLayerGroup, useMutationFlow, or createStackHook
UI-agnostic layer stack engine for @stainless-code/layers. Use for opening layers imperatively from anywhere, ordered named stacks, typed await or fire-and-forget calls, lifecycle control, blockers, validation, singleton updates, serial or nested layers, multi-stack orchestration, or building a library/framework adapter.
Lit adapter for @stainless-code/lit-layers; open UI from anywhere and manage ordered, named stacks with typed or fire-and-forget `open` via Reactive Controllers and custom elements
Preact adapter for @stainless-code/preact-layers — use for awaitable confirm flows, singleton toasts, nested drawers, stack subscriptions, and custom layer hosts
React adapter for @stainless-code/react-layers; open UI from anywhere and manage ordered, named stacks with typed or fire-and-forget `open`
| name | vue-layers |
| description | Vue adapter for @stainless-code/vue-layers; open UI from anywhere and manage ordered, named stacks with typed or fire-and-forget `open` |
| license | MIT |
| keywords | ["tanstack-intent","vue","modal","dialog","stack","typescript"] |
| metadata | {"library":"@stainless-code/vue-layers","library_version":"0.2.4","framework":"vue"} |
| sources | ["https://stainless-code.com/layers/adapters/vue","https://github.com/stainless-code/layers/blob/main/docs/architecture.md"] |
Open any layer from anywhere and manage modal, dialog, drawer, popover, or toast UI as an ordered, named stack. @stainless-code/vue-layers is the Vue 3 adapter for @stainless-code/layers; awaiting a typed result is optional, and fire-and-forget invocation (void client.open(...)) is equally first-class.
Named stacks, singletons with upsert and live update, serial queues, nested stacks, transitions, dismissal blockers, payload validation, and headless rendering each provide standalone value. The package re-exports @stainless-code/layers, so adapter and core APIs share one import path. For engine internals, use the layers core skill.
@stainless-code/vue-layers to open overlay UI imperatively from anywhere—components, composables, route guards, or non-UI code—and manage it as an ordered, named stack instead of prop-drilling isOpen, lifting state, or threading onConfirm callbacks.upsert) with live update, or one-at-a-time queues; nested stacks (useLayerGroup), enter/exit animations, dismissal guards (blockers), payload validation, or headless rendering (useStackHandles).Skip it only when: you have a single, always-local overlay opened from one component, with no return, stacking, queue, animation, or guard needs and no wish for a global registry.
Full fit matrix: When to use Layers.
bun add @stainless-code/vue-layers
Core is included. vue (>=3.3) is a required peer and should already be installed by the app.
<!-- ConfirmDialog.vue -->
<script setup lang="ts">
import type { LayerComponentProps } from "@stainless-code/vue-layers";
import type { ConfirmPayload, ConfirmResponse } from "./confirm";
const { call, payload } =
defineProps<LayerComponentProps<ConfirmPayload, ConfirmResponse>>();
</script>
<template>
<div role="dialog">
<h2>{{ payload.title }}</h2>
<button type="button" @click="call.end(true)">Yes</button>
<button type="button" @click="call.end(false)">No</button>
</div>
</template>
// confirm.ts
import { layerOptions } from "@stainless-code/vue-layers";
import ConfirmDialog from "./ConfirmDialog.vue";
export type ConfirmPayload = { title: string };
export type ConfirmResponse = boolean;
export const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({
stack: "confirm",
key: ["confirm", "remove"],
component: ConfirmDialog,
exitingDelay: 200,
});
<!-- App.vue — mount the stack outlet once, high in the tree -->
<script setup lang="ts">
import { provideLayerClient, StackOutlet } from "@stainless-code/vue-layers";
provideLayerClient();
</script>
<template>
<RouterView />
<StackOutlet stack="confirm" />
</template>
<!-- Opener.vue -->
<script setup lang="ts">
import { useLayer } from "@stainless-code/vue-layers";
import { confirm } from "./confirm";
const c = useLayer(confirm);
async function handleRemove() {
const ok = await c.open({ title: "Remove?" });
if (!ok) return;
deleteItem();
}
</script>
<template>
<button type="button" @click="handleRemove()">Remove</button>
</template>
Low-level bag-form: useLayerClient() + client.open({ ...confirm, payload }).
payload is optional when its type is void, unknown, undefined, or a union containing undefined; responses default to void. This no-payload, fire-and-forget layer omits payload, does not await the returned promise, and dismisses without a response:
<!-- NoticeDialog.vue -->
<script setup lang="ts">
import type { LayerComponentProps } from "@stainless-code/vue-layers";
const { call } = defineProps<LayerComponentProps<void>>();
</script>
<template>
<div role="status">
Changes saved.
<button type="button" @click="call.dismiss()">Close</button>
</div>
</template>
// notice.ts
import { layerOptions } from "@stainless-code/vue-layers";
import NoticeDialog from "./NoticeDialog.vue";
export const notice = layerOptions<void>({
stack: "notice",
key: ["notice", "saved"],
component: NoticeDialog,
});
<!-- FireAndForgetOpener.vue -->
<script setup lang="ts">
import { useLayer } from "@stainless-code/vue-layers";
import { notice } from "./notice";
const c = useLayer(notice);
function showNotice() {
void c.open();
}
</script>
<template>
<button type="button" @click="showNotice()">Save</button>
</template>
call contextEach layer component receives call (end/dismiss/update/setRunning/settle/ended/index/stackSize/root/stackId/layerId/addBlocker), payload, data, error, phase, transition, actionStatus, dismissing. Use await call.end(response) to resolve the caller's await and dismiss the layer (Promise<boolean> — false if vetoed). setRunning(true|false) flips actionStatus manually; useMutationFlow (below) wraps setRunning + end for the common save-then-close case.
Key vs id: key is the logical identity (find/upsert/gcTime); each mount gets a unique instance id. Use s.id for v-for keys; parallel stacks may hold multiple same-key layers.
const c = useLayer(confirm);
await c.open({ title: "Remove?" });
Options-bag + optional trailing client; select (not selector). Returns Readonly<Ref<T>>:
<script setup lang="ts">
import { useStack, useQueuedStack } from "@stainless-code/vue-layers";
const stack = useStack({ stack: "confirm" });
const count = useStack({ stack: "confirm", select: (s) => s.length });
const queued = useQueuedStack({ stack: "confirm" });
</script>
Observe-only; Readonly<Ref<LayerState[]>> for all same-key instances:
<script setup lang="ts">
import { useLayerState } from "@stainless-code/vue-layers";
import { confirm } from "./confirm";
const states = useLayerState({ key: confirm.key, stack: "confirm" });
const top = computed(() => states.value.at(-1));
</script>
Optional select and compare (default Object.is) limit ref updates when only a slice matters.
Isolate a subscription in a scoped-slot component. The slot payload is { value: unknown } — prefer useStack({ select }) in setup() for a fully typed ref:
<script setup lang="ts">
import { StackSubscribe } from "@stainless-code/vue-layers";
</script>
<template>
<StackSubscribe stack="confirm" :selector="(s) => s.length">
<template #default="{ value }">{{ value }} open</template>
</StackSubscribe>
</template>
StackOutlet is the default host. It maps each state to its registered component with full props (call, payload, data, error, phase, transition, actionStatus, dismissing). A missing component renders nothing and warns in development.
StackOutlet renders inline. Wrap it in <Teleport to="body"> when you need a portal target.
When you need custom hosts (switch by key, your own wrappers, alternate DOM slots):
<script setup lang="ts">
import { useStackHandles } from "@stainless-code/vue-layers";
const { states, getCall } = useStackHandles("modal");
</script>
<template>
<template v-for="s in states" :key="s.id">
<Sheet
v-if="s.key[0] === 'sheet'"
:call="getCall(s)"
:payload="s.payload"
/>
<Dialog v-else :call="getCall(s)" :payload="s.payload" />
</template>
</template>
states is Readonly<Ref<LayerState[]>>; getCall(state) returns the layer's LayerCallContext.
Inside a layer component, useLayerGroup(call) owns a child stack scoped to the parent's lifetime — parent dismiss / unmount cancelAlls it (LayerCancelledError).
<!-- SettingsDrawer.vue -->
<script setup lang="ts">
import {
useLayerGroup,
layerOptions,
type LayerComponentProps,
} from "@stainless-code/vue-layers";
import AdvancedPanel from "./AdvancedPanel.vue";
const nested = layerOptions<{ label: string }>({
key: ["settings", "advanced"],
component: AdvancedPanel,
});
const { call, payload } =
defineProps<LayerComponentProps<{ title: string }, boolean>>();
const group = useLayerGroup(call);
</script>
<template>
<div role="dialog" :aria-label="payload.title">
<button
type="button"
@click="group.open({ ...nested, payload: { label: 'Advanced' } })"
>
Open nested
</button>
<group.Outlet />
<button type="button" @click="call.dismiss(false)">Close</button>
</div>
</template>
Returns { open, dismissAll, states, Outlet, stackId }. open pre-binds the child stack id.
Drive a layer's actionStatus while an async mutation runs, then end the layer on success:
<!-- ConfirmSave.vue -->
<script setup lang="ts">
import {
useMutationFlow,
type LayerComponentProps,
} from "@stainless-code/vue-layers";
const { call, payload } =
defineProps<LayerComponentProps<{ name: string }, boolean>>();
const flow = useMutationFlow(call);
async function save() {
await flow
.run(async () => {
await saveToServer(payload.name);
})
.orEnd(true);
}
</script>
<template>
<div role="dialog">
<p>Save {{ payload.name }}?</p>
<button type="button" :disabled="flow.pending" @click="save()">
{{ flow.pending ? "Saving…" : "Save" }}
</button>
<button type="button" @click="call.end(false)">Cancel</button>
</div>
</template>
If the mutation throws, orEnd restores the idle status, leaves the layer open, and rethrows. pending is Readonly<Ref<boolean>> — use flow.pending.value in script or flow.pending in templates.
Bind a stack id, optional client, and optional host wrapper once:
// app-stack.ts
import { createStackHook } from "@stainless-code/vue-layers";
export const { StackProvider, useAppStack, AppHost, AppLayer } =
createStackHook({ stack: "modal" });
<!-- App.vue -->
<script setup lang="ts">
import SettingsDialog from "./SettingsDialog.vue";
import { StackProvider, AppHost } from "./app-stack";
</script>
<template>
<StackProvider>
<AppHost />
<ModalTrigger />
</StackProvider>
</template>
<!-- ModalTrigger.vue -->
<script setup lang="ts">
import SettingsDialog from "./SettingsDialog.vue";
import { useAppStack } from "./app-stack";
const { open } = useAppStack();
</script>
<template>
<button
type="button"
@click="
open({
key: ['modal', 'settings'],
component: SettingsDialog,
})
"
>
Open settings
</button>
</template>
useAppStack() — { open, dismissAll, states } with stack pre-bound; states is a ref.AppHost — renders StackOutlet for the bound stack. Host props are forwarded via fallthrough attrs as rootProps; an optional configured Host receives the same attrs and wraps the outlet.AppLayer — controlled open/close via props (open, payload, options, onResolved):<script setup lang="ts">
import { AppLayer } from "./app-stack";
import { settingsOpts } from "./settings";
defineProps<{ open: boolean }>();
const emit = defineEmits<{ close: [] }>();
</script>
<template>
<AppLayer
:options="settingsOpts"
:open="open"
:payload="{ title: 'Settings' }"
:on-resolved="() => emit('close')"
/>
</template>
| Kind | Name | Signature / shape |
|---|---|---|
| Function | provideLayerClient | (client?: LayerClient) => LayerClient |
| Hook | useLayerClient | () => LayerClient |
| Hook | useLayer | (options, client?) => WiredLayerHandle refs + state/queued/top |
| Hook | useLayerState | ({ key, stack?, select?, compare? }, client?) => Readonly<Ref<LayerState[]>> |
| Hook | useLayerQueuedState | ({ key, stack?, select?, compare? }, client?) => Readonly<Ref<LayerState[]>> |
| Hook | useStack | ({ stack?, select?, compare? }, client?) => Readonly<Ref<T>> |
| Hook | useQueuedStack | ({ stack?, select?, compare? }, client?) => Readonly<Ref<T>> |
| Hook | useStackHandles | (stack?, rootProps?) => StackHandles |
| Component | StackOutlet | { stack?, rootProps? } |
| Component | StackSubscribe | { stack?, selector } — scoped slot { value: unknown } |
| Hook | useMutationFlow | <P, R, RootProps?>(call) => MutationFlow<R> |
| Hook | useLayerGroup | <P, R, RootProps?>(call, options?) => LayerGroup |
| Factory | createStackHook | <HostProps>({ stack?, client?, Host? }) => StackHook<HostProps> |
| Type | StackHandles | { states: Readonly<Ref<LayerState[]>>, getCall } |
| Type | MutationRun<R> | { orEnd(response) } |
| Type | MutationFlow<R> | { pending: Readonly<Ref<boolean>>, run } |
| Type | ScopedOpen | open with stack pre-bound |
| Type | LayerGroup | { open, dismissAll, states, Outlet, stackId } |
| Type | AppStack | { open, dismissAll, states } |
| Type | AppLayerProps<P, R> | { options, open, payload, onResolved? } |
| Type | StackHook<HostProps> | { StackProvider, useAppStack, AppHost, AppLayer } |
Client-less subscriptions use useLayerClient() internally. Call useStack, useLayer, useStackHandles, useMutationFlow, and useLayerGroup inside setup() or effectScope(); subscriptions clean up via onScopeDispose.
Core exports are available from @stainless-code/vue-layers, including LayerClient, LayerStack, layerOptions, layerKey, LayerState, LayerComponentProps, LayerCallContext, createLayerGroup, DataTag, ResponseOf, and ErrorOf:
layerOptions / layerKey DataTag branding — await client.open(...) infers R.upsert: true on open; client.getStack(id).update(layer, patch).new LayerClient({ defaultStackOptions: { confirm: { scope: { strategy: "serial" } } } }).validate on layerOptions or open; narrow PayloadValidationError via isPayloadValidationError.call.addBlocker / stack.addBlocker; dismissing flag; dismissAll modes.See the layers skill for full engine coverage (transitions, blockers, validation, serial scope, multi-stack).