Modals are just async functions you forgot to await.
Alpine.js adapter — open any layer from anywhere and await a typed result. State coordination, not UI ownership: you own markup, focus (@alpinejs/focus), portals (x-teleport), and a11y.
Experimental — the API may change between minor releases. Pin your version.
bun add @stainless-code/alpine-layers
Peer: alpinejs (>=3.13). Optional: @alpinejs/focus for modal trap recipes.
Load before Alpine core; the CDN entry listens for alpine:init and registers the plugin.
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@stainless-code/alpine-layers/cdn"
></script>
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"
></script>import Alpine from "alpinejs";
import layers, {
LayerClient,
createLayer,
layerOptions,
setLayerClient,
} from "@stainless-code/alpine-layers";
const client = new LayerClient();
setLayerClient(client);
Alpine.plugin(layers);
Alpine.start();<div x-data>
<template x-layer-outlet="'modal'">
<div x-data role="dialog">
<h2 x-text="$layer.payload.title"></h2>
<button type="button" @click="$layer.call.end(true)">Yes</button>
</div>
</template>
</div>const confirm = layerOptions({
stack: "modal",
key: ["confirm"],
exitingDelay: 0,
});
const c = createLayer(confirm, client);
void c.open({ title: "Remove?" });