MarkupRefineLib

Menu


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.

ClassPurpose
.mr-layer__headerTwo-column title/close-control header.
.mr-layer__headingWrapper for title and optional description.
.mr-layer__titleRemoves default heading block margins inside the scaffold.
.mr-layer__descriptionSecondary explanatory text under the title.
.mr-layer__bodyMain content region with safe intrinsic sizing.
.mr-layer__actionsWrapping 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.

Profile settings

This example uses only native HTML plus public Layer presentation classes.

The dialog remains natively open while its closing transition runs, then Layer commits the native close.

<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.

Navigation

Same behavior contract, edge-attached presentation.

<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.