Skip to content

Tailwind Modal

Modals interrupt the user's flow to request focused attention, which makes accessibility especially important. This lesson builds a complete Tailwind modal, covering the overlay, positioning, and keyboard behavior.

What Makes Up a Tailwind Modal?

A modal combines several concepts from earlier lessons: fixed inset-0 for a full-screen overlay, flex items-center justify-center to center the dialog, a high z-* value to layer above everything else, and a semi-transparent backdrop using a color opacity modifier.

Beyond styling, a fully accessible modal also needs to trap keyboard focus inside itself while open, and close on both an explicit close button and the Escape key.

<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
  <div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
    <h2 class="text-lg font-semibold">Modal Title</h2>
    <p class="mt-2 text-sm text-slate-600">Modal content goes here.</p>
  </div>
</div>

fixed inset-0 covers the whole viewport, and flex items-center justify-center perfectly centers the dialog box.

Modal Structure Syntax

<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
  <div role="dialog" aria-modal="true" class="rounded-xl bg-white p-6">
    <!-- dialog content -->
  </div>
</div>
  • fixed inset-0 creates a full-viewport overlay layer.
  • bg-black/50 dims the page content behind the modal.
  • role="dialog" and aria-modal="true" mark the dialog for assistive technology.
  • A high z-* value (like z-50) ensures the modal renders above the rest of the page.

Modal Cheatsheet

Utility combinations for building a complete modal component.

Piece Classes Purpose
Overlay fixed inset-0 z-50 bg-black/50 Full-screen dimmed backdrop
Centering wrapper flex items-center justify-center Centers the dialog on the overlay
Dialog box w-full max-w-md rounded-xl bg-white p-6 shadow-xl The visible modal content container
Close button absolute right-4 top-4 Positioned close icon inside the dialog
Scroll lock overflow-hidden on <body> via JS Prevents background page scrolling while open
Enter animation transition-opacity + JS toggle Fades the modal in smoothly
Mobile padding p-4 on the overlay Keeps the dialog off-screen edges on small devices
Footer actions flex justify-end gap-3 mt-6 Aligns confirm/cancel buttons

Complete Modal Markup

Putting the pieces together: an overlay, a centered dialog, a heading, body content, and footer actions form a complete, reusable modal structure.

<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
  <div role="dialog" aria-modal="true" aria-labelledby="modal-title" class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
    <div class="flex items-start justify-between">
      <h2 id="modal-title" class="text-lg font-semibold text-slate-900">Delete Item?</h2>
      <button aria-label="Close" class="text-slate-400 hover:text-slate-600">✕</button>
    </div>
    <p class="mt-2 text-sm text-slate-600">This action cannot be undone.</p>
    <div class="mt-6 flex justify-end gap-3">
      <button class="rounded-lg px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100">Cancel</button>
      <button class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700">Delete</button>
    </div>
  </div>
</div>

Closing on Escape and Backdrop Click

Users expect a modal to close when they press Escape or click the dimmed backdrop outside the dialog box, in addition to an explicit close button.

<script>
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape") closeModal();
  });

  document.querySelector("[data-modal-overlay]").addEventListener("click", (e) => {
    if (e.target === e.currentTarget) closeModal();
  });
</script>

Checking e.target === e.currentTarget ensures clicks inside the dialog box itself don't accidentally close the modal.

Trapping Focus Inside the Modal

While a modal is open, Tab and Shift+Tab should cycle only through focusable elements inside the dialog, never back to the underlying page, and focus should move to the modal automatically when it opens, then return to the triggering element when it closes.

  • Move focus to the modal's first focusable element (or the dialog itself) when it opens.
  • Trap Tab/Shift+Tab navigation within the modal's focusable elements while it's open.
  • Return focus to the element that triggered the modal once it closes.
  • Consider using a well-tested library (like Headless UI's Dialog) instead of hand-rolling focus trapping.

Accessible Modal Requirements

Modals are one of the trickiest components to make fully accessible by hand. At minimum, they need proper ARIA roles, focus management, and full keyboard operability.

<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
  <h2 id="modal-title">Dialog Title</h2>
</div>
  • Use role="dialog", aria-modal="true", and aria-labelledby pointing to the dialog's heading.
  • Trap focus inside the modal while it's open and restore it afterward.
  • Always support closing via Escape, not just a visible close button.

Common Mistakes

  • Building a modal with no focus trapping, letting keyboard users tab into the hidden background page.
  • Forgetting to lock body scroll while the modal is open, causing a confusing double-scroll experience.
  • Not closing the modal on Escape or backdrop click, only on an explicit close button.
  • Using a low z-index that lets other fixed or sticky elements render above the modal.
  • Hand-rolling complex modal accessibility from scratch instead of using a proven library like Headless UI when the project already uses a JS framework.

Key Takeaways

  • A modal combines a fixed full-screen overlay, a centered dialog box, and a high z-index layer.
  • Modals must support closing via Escape and backdrop click, not just an explicit close button.
  • Focus should move into the modal on open and trap Tab navigation until it closes.
  • role="dialog", aria-modal="true", and aria-labelledby are the minimum required ARIA attributes.
  • Consider a tested accessibility library instead of hand-rolling focus trapping for production applications.

Pro Tip

If your project already uses React, Vue, or a similar framework, use a library like Headless UI or Radix for the modal's behavior and accessibility, then style it entirely with your own Tailwind classes, you get correct focus trapping without maintaining it yourself.