iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Modals

A solid modal in 2026 should use the native `

` element (it manages focus + scroll lock + Escape + backdrop for you) and add Tailwind for the styling. Reach for Headless UI / Radix only when you need controlled state across many panels.

Native dialog + Tailwind, with focus trap and backdrop

EXAMPLE
<!-- 1) Native <dialog> — minimal, accessible -->
<button class='btn' onclick='document.getElementById("contact").showModal()'>Contact us</button>

<dialog id='contact'
        class='m-auto w-[min(420px,calc(100vw-2rem))] rounded-xl border bg-white p-6 shadow-xl
                 backdrop:bg-black/40 dark:bg-slate-900 dark:border-slate-700'>
  <form method='dialog' class='space-y-3'>
    <h2 class='text-lg font-semibold'>Contact us</h2>
    <label class='block'>
      <span class='text-sm'>Email</span>
      <input type='email' name='email' required class='input' />
    </label>
    <label class='block'>
      <span class='text-sm'>Message</span>
      <textarea name='message' required class='input' rows='4'></textarea>
    </label>
    <div class='flex justify-end gap-2 pt-2'>
      <button value='cancel' class='rounded px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-800'>Cancel</button>
      <button value='send'   class='rounded bg-blue-600 px-3 py-1.5 text-white hover:bg-blue-700'>Send</button>
    </div>
  </form>
</dialog>

<!-- The browser handles:
- Backdrop (::backdrop pseudo-element)
- Escape to close
- Focus trapping while open
- Top-layer rendering (always above other content)
- Inert background (clicks outside ignored)
-->

<!-- 2) Click backdrop to close -->
<script>
  document.querySelectorAll('dialog').forEach((d) => {
    d.addEventListener('click', (e) => {
      const r = d.getBoundingClientRect();
      const inside = e.clientX >= r.left && e.clientX <= r.right &&
                      e.clientY >= r.top  && e.clientY <= r.bottom;
      if (!inside) d.close('cancel');
    });
  });
</script>

<!-- 3) Animations via the dialog open/close -->
<style>
  dialog[open] {
    animation: pop .15s ease-out;
  }
  dialog::backdrop { transition: opacity .15s ease; }
  @keyframes pop { from { transform: scale(.97); opacity: 0; } to { transform: scale(1); opacity: 1; } }
</style>

<!-- 4) Confirmation modal (return value via form method='dialog') -->
<button onclick='document.getElementById("confirm").showModal()'>Delete</button>
<dialog id='confirm' class='rounded-xl bg-white p-6 backdrop:bg-black/40'>
  <form method='dialog' class='space-y-3'>
    <p>Delete this order? This cannot be undone.</p>
    <div class='flex justify-end gap-2'>
      <button value='cancel' class='rounded px-3 py-1.5 hover:bg-slate-100'>Cancel</button>
      <button value='delete' class='rounded bg-red-600 px-3 py-1.5 text-white hover:bg-red-700'>Delete</button>
    </div>
  </form>
</dialog>
<script>
  document.getElementById('confirm').addEventListener('close', (e) => {
    if (e.target.returnValue === 'delete') {
      // do delete
    }
  });
</script>

<!-- 5) Mobile-friendly bottom sheet -->
<dialog id='sheet'
        class='m-0 mt-auto w-full rounded-t-xl bg-white p-4 max-h-[80vh]
                backdrop:bg-black/40 sm:m-auto sm:rounded-xl sm:max-w-md sm:max-h-[80vh]'>
  ...
</dialog>

<!-- 6) Long content with internal scrolling -->
<dialog class='m-auto rounded-xl bg-white p-0 max-h-[80vh] flex flex-col'>
  <header class='border-b p-4'><h2>Long form</h2></header>
  <div class='overflow-auto p-4'>
    <!-- many fields -->
  </div>
  <footer class='border-t p-3 flex justify-end gap-2'>
    <button class='btn'>Save</button>
  </footer>
</dialog>

<!-- 7) When to reach for a library -->
<!-- @headlessui/react Dialog -->
<!-- @radix-ui/react-dialog -->
<!-- @ariakit/react        Dialog -->
<!-- Use a library when:
- You need controlled open state from multiple components
- You support old browsers (Safari < 15.4)
- You need declarative React/Vue/Svelte bindings
- You build many dialog variants and want consistency
-->

<!-- 8) Pitfalls -->
<!-- - dialog without showModal() (just .show()) -> no backdrop, no focus trap -->
<!-- - Backdrop click handler that fires even when clicking the dialog body
       -> use getBoundingClientRect check above -->
<!-- - Inputs that focus offscreen on iOS -> add scrollIntoView in open handler -->
<!-- - Long content without overflow-auto inside the dialog -> page-level scroll under backdrop -->
<!-- - body { overflow: hidden } toggled by hand -> dialog already inerts background -->

Why it matters

The native `

` element with `showModal()` covers focus trap, backdrop, Escape handling, top-layer rendering, and inert background for free. Use it as the default and reach for a JS dialog library only when you need controlled open state, animations beyond CSS, or compatibility with browsers that predate dialog support.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
<!-- Use <dialog>! -->
<dialog class="backdrop:bg-black/50 rounded-lg p-6">…</dialog>
Try it Yourself »

Discussion

Loading…