Modals
A solid modal in 2026 should use the native `
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 `
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…