Stable Layer API
This page is the stable public contract for Markup Refine Layers. The runtime is intentionally small: native HTML owns modal/popover semantics, Markup Refine coordinates local lifecycle and ownership, and optional remote navigation remains a separate focused export.
Public responsibility split
| Category | Stable contract |
|---|---|
| Presentation classes | .mr-layer, .mr-layer--modal, .mr-layer--drawer, and .mr-layer--popover style surfaces only. Layer behavior does not query them. |
| Behavior hooks | data-mr-layer, data-mr-tooltip, and the optional data-mr-layer-navigation marker identify enhancement. data-mr-preserve is the remote-validation state-preservation hook and requires a stable id. These hooks do not encode application workflows. |
| Native state | <dialog open>, :popover-open, native cancel/close/beforetoggle/toggle, and ordinary link/form behavior remain source-of-truth platform state. |
| ARIA state | Labels, aria-expanded, aria-controls, aria-describedby, aria-busy, and validation state communicate semantics; they are not a second Layer state engine. |
| Lifecycle events | mr:layer:beforeopen, mr:layer:open, mr:layer:beforeclose, mr:layer:close, and mr:layer:result are for observation/integration. Promise results remain the workflow API. |
Native surfaces and presentation primitives
Layer mode is constrained by the native surface: modal and drawer Layers require a native <dialog>; a popover Layer requires an element with the nativepopover attribute/API. Markup Refine does not emulate missing top-layer capabilities.
| Public class | Meaning |
|---|---|
.mr-layer | Shared optional surface presentation. |
.mr-layer--modal | Centered dialog presentation. |
.mr-layer--drawer | Edge-attached presentation on the same modal dialog lifecycle. |
.mr-layer--popover | Non-modal native Popover presentation. |
.mr-layer__header, __heading, __title, __description, __body, __actions | Optional content scaffold shared by every Layer mode. These classes never initialize Layer behavior. |
Tooltip source content additionally has optional .mr-tooltip__title and.mr-tooltip__content presentation primitives. Tooltip preview/pinned surface classes are created by the Tooltip runtime; consumers normally author the trigger and source <template>, not those generated surfaces.
JavaScript API
Public exports
import {
initLayers,
layers,
type Layer,
type LayerManager,
type LayerMode,
type LayerState,
type LayerOpenOptions,
type LayerCloseOptions,
type LayerOutcome,
type LayerLifecycleEventDetail,
type LayerResultEventDetail,
} from "markup-refine-lib/layers";import { initLayers, layers } from "markup-refine-lib/layers";
initLayers(document);
const modal = layers.get(document.querySelector("#settings")!);
await modal?.open({ trigger: settingsButton });
const outcome = await layers.ask<Choice>("#picker", { trigger: pickerButton });
if (outcome.status === "accepted") {
applyChoice(outcome.value);
}The stable Layer object exposes element, mode,state, trigger, parent, andchildren, plus open(), close(),accept(), dismiss(), and ask(). The manager adds get(), closest(), open(),ask(), current, and stack.
Stable signatures
initLayers(root?: ParentNode): void
layers.get(target: Element | string): Layer | undefined
layers.closest(node: Node): Layer | null
layers.open(target, { trigger?, parent? }): Promise<Layer>
layers.ask<T, R>(target, { trigger?, parent? }): Promise<LayerOutcome<T, R>>
layers.current: Layer | null
layers.stack: readonly Layer[]
layer.open({ trigger?, parent? }): Promise<void>
layer.close({ restoreFocus? }): Promise<void>
layer.accept<T>(value: T): Promise<void>
layer.dismiss<R>(reason?: R): Promise<void>
layer.ask<T, R>({ trigger?, parent? }): Promise<LayerOutcome<T, R>>parent is normally inferred from the trigger/invoker but can be supplied explicitly for orchestration. restoreFocus defaults to Layer-managed focus restoration for dialogs; callers may disable it for specialized close flows.
Stable public types
type LayerMode = "modal" | "drawer" | "popover";
type LayerState = "closed" | "opening" | "open" | "closing";
type LayerOutcome<T, R = unknown> =
| { status: "accepted"; value: T }
| { status: "dismissed"; reason?: R };
interface LayerOpenOptions {
trigger?: HTMLElement | null;
parent?: Layer | null;
}
interface LayerCloseOptions {
restoreFocus?: boolean;
}
interface LayerLifecycleEventDetail {
layer: Layer;
}
interface LayerResultEventDetail<T = unknown, R = unknown> {
layer: Layer;
outcome: LayerOutcome<T, R>;
}layers.get() lazily registers a declarative Layer when possible, andinitLayers(root) is safe to call repeatedly for SSR/partial-rendered subtrees. layers.closest(node) finds the nearest containing registered (or declaratively registerable) Layer.
Declarative HTML API
<button commandfor="settings" command="show-modal">Settings</button>
<a href="/settings">Settings page fallback</a>
<dialog id="settings"
data-mr-layer="modal"
aria-labelledby="settings-title">
<h2 id="settings-title">Settings</h2>
<button commandfor="settings" command="request-close">Close</button>
</dialog>Prefer native commandfor/command andpopovertarget when a control directly owns a native surface. The normal link in the example is the lower-capability path for an essential task; Markup Refine does not hide it before enhancement.
Closing and exit motion
A Markup Refine-controlled close does not remove the native surface immediately. The Layer enters closing, exposes the transientdata-mr-layer-closing CSS hook, waits for finite CSS transitions or animations running on the Layer surface (and dialog backdrop where supported), and then commits the native close. This keeps animation duration in CSS rather than in a duplicated JavaScript timeout.
Backdrop dismissal
A click on the backdrop/outside any active modal or drawer is treated as a global exit from the transient Layer workflow. The Layer manager consumes that click and closes every active Layer in the same document, including nested modal/drawer/popover/Tooltip Layers. This prevents a backdrop click from closing only the top child and also prevents click-through into page content exposed while the stack is closing.
Lifecycle events
Lifecycle events bubble from the Layer surface. The four lifecycle events carrydetail.layer; mr:layer:result carriesdetail.layer plus detail.outcome. Use these events to observe or integrate with Layer activity, not as a second state store.
| Event | Cancelable | Use |
|---|---|---|
mr:layer:beforeopen | Yes | Veto a programmatic/native opening request before the Layer commits. |
mr:layer:open | No | Observe settled open state. |
mr:layer:beforeclose | Only where the native close request is cancelable | Observe/veto a native close request such as dialog cancellation. |
mr:layer:close | No | Observe settled close state after ownership/focus cleanup. |
mr:layer:result | No | Observe an accepted/dismissed outcome. Direct callers should normally await ask(). |
A programmatic layer.close() emits mr:layer:beforeclose as a non-cancelable observation. Native dialog requestClose()/Escape reaches a cancelable cancel path, so mr:layer:beforeclose may veto that close request. A direct unconditional dialog.close() happens natively and cannot be retroactively delayed or canceled.
Ownership and nested Layers
When a Layer opens, parent ownership is inferred from the actual trigger/invoker: if that trigger lives inside an active Layer, the new Layer becomes its child. Supplyingparent explicitly is reserved for orchestration that cannot be represented by the invoker relationship. Closing a parent closes descendants child-first; closing or accepting a child does not complete its parent.
Graceful-degradation matrix
| Capability/failure | Stable behavior | Author fallback |
|---|---|---|
| JavaScript unavailable | No Markup Refine interception or generated workflow runs. | Use ordinary links/forms/details/inline content for essential tasks. |
| CSS unavailable | Layer behavior still uses native dialog/Popover semantics; Markup Refine presentation is simply absent. | Keep semantic labels, controls, and native close paths in the markup. |
| Dialog API unavailable | Programmatic modal/drawer opening fails before a Layer is committed; declarative remote links fall back to normal navigation. | Essential local modal actions need an explicit inline or navigation fallback. |
| Popover API unavailable | Popover Layer opening fails rather than silently changing interaction modality. | Use inline/<details> fallback where the content is essential. |
| Initialization fails before commit | The baseline control remains untouched and may be initialized again later. | Normal HTML remains usable. |
| Initialization fails after preparation | Generated/opening state is rolled back; optional remote declarative GET enhancement returns to the original navigation path. | Do not hide the baseline solely with pre-enhancement CSS. |
| Late initialization | Registration observes an already-open native dialog/popover state rather than forcibly resetting it. | No special fallback is required. |
| Remote initial GET fails | Declaratively enhanced links report an error then navigate to their real href. | The destination must be a valid standalone page. |
| Remote mutation fails | The current Layer remains open, emits an error, and does not automatically replay the mutation. | Application/server UI decides how to retry safely. |
Markup Refine never automatically promotes an unsupported popover to a modal. A change in modality must be an explicit application choice. No generic enhancement-success attribute is part of the public API: an enhancement is committed only when its native/listener operation succeeds, so fallback content never needs to be pre-hidden by a styling marker.
Lower-capability Popover example
<button popovertarget="format-help">Formatting help</button>
<div id="format-help" popover="auto" data-mr-layer="popover">
Supplementary formatting help.
</div>
<details>
<summary>Formatting help without Popover</summary>
<p>Supplementary formatting help.</p>
</details>Tooltip text is supplementary by contract, so essential meaning must already be in ordinary page content. Interactive popover content that is essential should have an authored inline/disclosure/navigation path like the example above.
Custom CSS and Tailwind
Layer behavior depends on data-mr-layer and native semantics, not on Markup Refine presentation classes. Applications may therefore supply their own CSS or Tailwind classes without changing the Layer runtime.
Custom CSS
<dialog id="custom-layer"
data-mr-layer="modal"
class="app-dialog"
aria-labelledby="custom-layer-title">
<h2 id="custom-layer-title">Custom presentation</h2>
...
</dialog>
<style>
.app-dialog {
inline-size: min(42rem, calc(100vi - 2rem));
border: 1px solid currentColor;
border-radius: 1rem;
padding: 1.5rem;
}
</style>Tailwind
<dialog id="tailwind-layer"
data-mr-layer="modal"
class="max-w-xl rounded-xl border p-6 shadow-xl"
aria-labelledby="tailwind-layer-title">
<h2 id="tailwind-layer-title" class="text-xl font-semibold">Tailwind presentation</h2>
...
</dialog>Tailwind is not a dependency of the Layer package; this is only a presentation example.
Component integration
- Search Tool: search owns fetching/query/results; Layer owns modal lifecycle, native backdrop/light-dismiss compatibility, and trigger focus restoration.
- Application Shell: shell owns navigation/responsive content transfer and disclosure ARIA; Layer owns the transient drawer lifecycle.
- Tooltips: transient hint previews remain outside the Layer stack; pinned interactive content becomes a native Popover Layer.
Local nested subinteractions
ask() composes Book → Author-style workflows recursively. A child's accepted/dismissed result completes only that child. The caller explicitly decides whether to update local DOM/state, refresh a specific server fragment, or reload a parent. Parent refresh is never implicit.
Remote navigation decision
Decision: keep the remote module as the optional focusedmarkup-refine-lib/layer-navigation export. Its stable scope is only same-origin, HTML-first modal subinteractions that produce the existingLayerOutcome. It does not become a general fragment/router/history framework. Applications needing broad target swapping, navigation/history, polling, or a richer overlay navigation system should use a dedicated server-driven library instead of expanding Layer core.
The remote module remains absent from markup-refine-lib/behaviors and Layer core has no fetch, URL, form, redirect, CSRF, or history dependency.
Remote Layer forms include basic same-origin multipart/form-data support so an inner file upload does not unexpectedly navigate away and discard parent Layer state. Validation-time DOM state can be retained explicitly with data-mr-preserve plus a stable id; file inputs are the primary use case because their selectedFileList cannot be recreated from returned HTML. This remains basic form submission, not a progress/chunking/resumable-upload subsystem.