Modal and drawer Layers
Modal and drawer Layers share one native <dialog> lifecycle.data-mr-layer opts into Layer behavior; themr-layer* classes opt into Markup Refine presentation. The drawer is therefore a visual variant rather than a second overlay state machine.
Reusable Layer content primitives
The surface class controls presentation of the top-layer element. The optionalmr-layer__* classes arrange authored content inside that surface and work identically in modal, drawer, and popover Layers. None of these classes enable behavior.
| Class | Purpose |
|---|---|
.mr-layer__header | Two-column title/close-control header. |
.mr-layer__heading | Wrapper for title and optional description. |
.mr-layer__title | Removes default heading block margins inside the scaffold. |
.mr-layer__description | Secondary explanatory text under the title. |
.mr-layer__body | Main content region with safe intrinsic sizing. |
.mr-layer__actions | Wrapping end-aligned action row with a visual separator. |
These are presentation primitives, not required DOM structure. Use semantic elements appropriate to the content: <header>, <main>,<section>, <form>, or ordinary <div>s are all valid.
Modal demonstration
showModal() places the dialog in the browser top layer and makes the rest of the containing document inert. Markup Refine does not add a custom focus trap or a separate backdrop element.
<button commandfor="settings" command="show-modal">Settings</button>
<dialog id="settings"
class="mr-layer mr-layer--modal"
data-mr-layer="modal"
aria-labelledby="settings-title">
<header class="mr-layer__header">
<div class="mr-layer__heading">
<h2 id="settings-title" class="mr-layer__title">Settings</h2>
<p class="mr-layer__description">Change local preferences for this example.</p>
</div>
<button commandfor="settings" command="request-close" aria-label="Close settings">
Close
</button>
</header>
<div class="mr-layer__body">
<label>
Display name
<input name="display-name" value="Ada" />
</label>
</div>
<footer class="mr-layer__actions">
<button commandfor="settings" command="request-close">Cancel</button>
<button type="button" class="mr-button--positive">Save example</button>
</footer>
</dialog>Drawer demonstration
A modal drawer is the same native dialog with data-mr-layer="drawer"and mr-layer--drawer. The presentation attaches the surface to the inline-start edge, while open/close, Escape, inertness, focus handling, and the top layer remain native dialog behavior.
<button commandfor="navigation" command="show-modal">Menu</button>
<dialog id="navigation"
class="mr-layer mr-layer--drawer"
data-mr-layer="drawer"
aria-labelledby="navigation-title">
<header class="mr-layer__header">
<div class="mr-layer__heading">
<h2 id="navigation-title" class="mr-layer__title">Navigation</h2>
<p class="mr-layer__description">A drawer is presentation on the same dialog lifecycle.</p>
</div>
<button commandfor="navigation" command="request-close">Close</button>
</header>
<div class="mr-layer__body">
<nav aria-label="Primary">...</nav>
</div>
</dialog>Native request-close semantics
Escape and command="request-close" use the dialog close-request path. That path fires a cancelable native cancel event. After the request is accepted, Layer keeps the native dialog open, setsdata-mr-layer-closing, waits for the real finite CSS exit transitions/animations, and only then commits the native close. Usecommand="close" only when the action must close unconditionally and intentionally bypass this coordinated exit path.
Declarative commandfor/command controls are optional. For browser targets where they are not suitable, call the focused Layer API from ordinary JavaScript instead; the surface markup and Layer lifecycle do not change.
import { initLayers, layers } from "markup-refine-lib/layers";
initLayers(document);
await layers.open("#settings", { trigger: openButton });
await layers.get("#settings")?.close();Presentation tokens
Layer sizing, viewport spacing, surface appearance, content spacing, backdrop, and motion are expressed through --mr-layer-* tokens. Override the tokens rather than replacing the Layer lifecycle.
:root {
--mr-layer-surface-inline-size: 42rem;
--mr-layer-drawer-inline-size: 20rem;
--mr-layer-surface-padding: var(--mr-space-md);
--mr-layer-header-gap: var(--mr-space-sm);
--mr-layer-section-gap: var(--mr-space-md);
--mr-layer-actions-gap: var(--mr-space-sm);
--mr-layer-backdrop-blur: 0;
--mr-layer-motion-duration: 160ms;
}Markup Refine disables Layer motion whenprefers-reduced-motion: reduce is active and switches the Layer surface/backdrop to system colors in forced-colors mode.
There is no JavaScript close-delay setting. layer.close() waits for the CSS motion actually running on the Layer surface (and dialog backdrop where supported), so changing --mr-layer-motion-duration or authoring a custom transition automatically changes how long the Layer stays natively open.
/* Custom presentation works too. */
[data-mr-layer="modal"] {
opacity: 0;
transition: opacity 180ms ease;
}
[data-mr-layer="modal"][open]:not([data-mr-layer-closing]) {
opacity: 1;
}Plain semantic dialog
Layer behavior and Layer presentation are both optional. A local dialog does not need Markup Refine classes, and standard HTML controls such as<form method="dialog"> remain valid.
<dialog id="plain-dialog">
<p>Plain native dialog content.</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>Lower-capability fallback
Essential actions must not exist only behind a Layer invoker. When a settings workflow has a standalone destination, keep that destination as ordinary HTML; Layer enhancement may sit beside it rather than replacing it.
<a href="/settings">Open settings page</a>
<!-- Enhanced browsers may additionally expose the local dialog control. -->
<button commandfor="settings" command="show-modal">Open settings dialog</button>If native modal-dialog capability is unavailable, Markup Refine does not emulate a top layer or focus trap. Use the authored navigation/inline fallback instead.