HTML + API · 04 of 05

Make room for a moment

Native modality, focus, and dismissal. This embedded preview is modal within its own frame.

dialog.showModal()Baseline Widely availableMDN showModal()
Experience kept
A modal flow with application-specific save logic
Machinery removed
Focus trapping, Escape handling, scroll lock, and z-index work
Decision
Keep app logic; return modality to the platform
Live demo

Open it. Tab through. Press Escape.

THE REDUCTION

Same outcome. Less to coordinate.

A custom overlay has to rebuild the top layer, focus containment, Escape, and focus return, then unwind all of it.

CURRENT PROJECT · JS
overlay.hidden = false;
document.body.style.overflow = "hidden";
document.addEventListener("keydown", closeOnEscape);
trapFocus(overlay);
// ...then undo all four on close, and put focus back on the trigger
WITH FEWER PARTS · JS
dialog.showModal();

dialog.addEventListener("close", () => {
  save(dialog.returnValue);
});

Only application logic stays in JavaScript. The skill still checks the accessible name, how the dialog is dismissed, and where focus lands afterwards.

ABOUT THE PATTERN

Why this works

The top layer, focus trapping, Escape handling, and backdrop semantics come from the platform instead of a custom overlay stack.

Decisions that matter

  • Name the dialog, choose initial focus deliberately, preserve Escape and return focus.
  • This minimal example disables its preview in unsupported browsers; production essential workflows need an alternative.
  • In the gallery iframe, modality affects only that frame.

Read from the skill's own reference files at build time.

COPY, ADAPT, SHIP

The dependency-free implementation

HTML, CSS, and the JavaScript the interaction actually needs

STANDALONE HTML
<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Native modal dialog</title>
<style id="recipe">
dialog { max-inline-size: min(28rem, calc(100% - 2rem)); }
dialog::backdrop { background: rgb(18 30 48 / 45%); }
</style>
<style>
*{box-sizing:border-box}body{margin:0;padding:28px;font:14px/1.5 system-ui;color:#182334;background:#f2f5f9;text-align:center}.tile{padding:30px 0}.symbol{font-size:32px;color:#3866e8}h2{font-size:18px;margin:8px 0}p{color:#647186;margin:8px 0 20px}button{font:inherit;font-weight:600;background:#202c42;color:white;border:0;border-radius:8px;padding:11px 20px;cursor:pointer}button:focus-visible{outline:3px solid #3866e8;outline-offset:4px}dialog{border:1px solid #ccd5e2;border-radius:16px;padding:24px;text-align:start;box-shadow:0 20px 70px #18233430}dialog p{color:#43536b}dialog form{text-align:end}
</style>
<div class="tile"><span class="symbol" aria-hidden="true">▣</span><h2>A moment of focus</h2><p>A native dialog, ready for the keyboard.</p><button id="open" type="button">Open dialog</button></div>
<dialog aria-labelledby="title"><h2 id="title">You're in the dialog.</h2><p>Press Escape or close to return to the page.</p><form method="dialog"><button autofocus>Close dialog</button></form></dialog>
<script>
const trigger = document.querySelector('#open');
const dialog = document.querySelector('dialog');
if (typeof dialog.showModal === 'function') {
  trigger.addEventListener('click', () => dialog.showModal());
  dialog.addEventListener('close', () => trigger.focus());
} else {
  trigger.disabled = true;
  trigger.textContent = 'Dialog preview unavailable';
}
</script></html>
Using Tailwind CSS v4 already? View the adapted example

Optional integration · adapt utilities to your project tokens

TAILWIND HTML
<!-- Tailwind CSS v4. JavaScript opens the native modal; CSS only styles it. -->
<section class="py-8 text-center">
  <span aria-hidden="true" class="text-3xl text-blue-600">▣</span>
  <h2 class="mt-2 text-lg font-semibold text-slate-900">A moment of focus</h2>
  <p class="mt-2 text-slate-600">A native dialog, ready for the keyboard.</p>
  <button id="open" type="button" class="mt-5 rounded-lg bg-slate-800 px-5 py-3 font-semibold text-white hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
    Open dialog
  </button>
</section>

<dialog aria-labelledby="dialog-title" class="m-auto w-[calc(100%-2rem)] max-w-md rounded-2xl border border-slate-300 bg-white p-6 text-left text-slate-900 shadow-2xl backdrop:bg-slate-900/45">
  <h2 id="dialog-title" class="text-lg font-semibold">You're in the dialog.</h2>
  <p class="mt-2 text-slate-600">Press Escape or close to return to the page.</p>
  <form method="dialog" class="mt-6 text-right">
    <button autofocus class="rounded-lg bg-slate-800 px-5 py-3 font-semibold text-white hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
      Close dialog
    </button>
  </form>
</dialog>

<script>
  const trigger = document.querySelector('#open');
  const dialog = document.querySelector('dialog');

  if (typeof dialog.showModal === 'function') {
    trigger.addEventListener('click', () => dialog.showModal());
    dialog.addEventListener('close', () => trigger.focus());
  } else {
    trigger.disabled = true;
    trigger.textContent = 'Dialog unavailable';
  }
</script>