36 Tailwind CSS Modals
01 / 36

All 36 in 36 Tailwind CSS Modals

  1. 01Tailwind Login / Signup Modal with Native Dialog & Starting-Style
  2. 02Tailwind Password Reset Modal with In-Place Success State
  3. 03Tailwind Age Verification Gate with :has() and Zero JavaScript
  4. 04Tailwind Paywall Modal with Masked Article Fade
  5. 05Tailwind Product Quick-View Modal with has-checked Variant Chips
  6. 06Tailwind Shopping Cart Slide-Over Drawer with Dialog
  7. 07Tailwind Timed Newsletter Promo Popup with Frequency Cap
  8. 08Tailwind Size Guide Overlay with Zero-JS cm/inch Toggle
  9. 09Tailwind Zero-JS Lightbox Gallery with the target Variant
  10. 10Tailwind Video Trailer Modal with Aspect-Locked Iframe
  11. 11Tailwind Destructive Delete Modal with Type-to-Confirm Gate
  12. 12Tailwind Success / Error Result Modal with Drawn SVG Icon
  13. 13Tailwind GDPR Cookie Consent Banner with details Disclosure
  14. 14Tailwind Multi-Step Onboarding Modal with data-step Variants
  15. 15Tailwind Filter & Sort Drawer that Becomes a Bottom Sheet
  16. 16Tailwind Contact Support Modal with Live Validation
  17. 17Tailwind Tabbed Settings Modal with Zero-JS Radio Tabs
  18. 18Tailwind Command Palette Modal with ⌘K and Roving Selection
  19. 19Tailwind Confirm Delete Modal with Corner Slide-and-Spin Entrance
  20. 20Tailwind Login Modal with Staggered Blur-In Content Drop
  21. 21Tailwind Signup Modal with 3D Origami Fan Deploy Panels
  22. 22Tailwind Payment Checkout Modal with 3D Paper Fold Entrance
  23. 23Tailwind HIPAA Patient Consent Modal with Scroll-Gated Agreement
  24. 24Tailwind Age Verification Modal with Clip-Path Iris Aperture
  25. 25Tailwind GDPR Cookie Preferences Modal with Unfolding Category Rows
  26. 26Tailwind Video Lightbox Modal with Chromatic Glitch Materialize
  27. 27Tailwind Image Gallery Lightbox with Clip-Path Diamond Reveal
  28. 28Tailwind Add to Cart Confirmation with SVG Liquid Morph Entrance
  29. 29Tailwind Product Quick View Modal with Elastic Button Morph
  30. 30Tailwind Newsletter Signup Modal with Depth-Layer Parallax Entrance
  31. 31Tailwind Exit-Intent Discount Modal with Live Countdown and Coupon Copy
  32. 32Tailwind Restaurant Reservation Modal with Cascading Fan Panels
  33. 33Tailwind Order Success Modal with CSS-Only Confetti Burst
  34. 34Tailwind SaaS Onboarding Welcome Modal with Cascading Blur-In
  35. 35Tailwind Terms of Service Modal with Letter-Slide and Consent Gate
  36. 36Tailwind Password Reset Modal with Enumeration-Safe Confirmation
Tailwind + JSMIT licensed

Tailwind Login / Signup Modal with Native Dialog & Starting-Style

The reference-grade auth overlay: one native <dialog> flips between Sign in and Create account with zero-JS tabs, floating labels and a blurring backdrop. The port's win is open: + open:starting: + transition-discrete — the closed state, the open state and the frame the browser paints before the transition all sit in one class list on the element that animates.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tm-01 w-full min-h-svh grid place-items-center bg-tm01-bg font-tm01-sans px-6 py-10">
  <button type="button" data-tm01-open class="inline-flex items-center justify-center min-h-11 px-7 rounded-xl text-[15px] font-semibold text-tm01-ink bg-tm01-btn border border-tm01-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm01-btnhover hover:border-tm01-acc focus-visible:outline-2 focus-visible:outline-tm01-acc focus-visible:outline-offset-2">Sign in</button>

  <dialog id="tm-01-dlg" aria-labelledby="tm-01-title" class="m-auto w-[min(400px,94vw)] p-0 border-0 rounded-[18px] text-tm01-ink bg-tm01-card shadow-[0_30px_80px_-30px_#000,0_0_0_1px_var(--color-tm01-edge)] opacity-0 translate-y-2.5 scale-[0.98] transition-[opacity,translate,scale,overlay,display] duration-300 ease-out transition-discrete open:opacity-100 open:translate-y-0 open:scale-100 open:starting:opacity-0 open:starting:translate-y-2.5 open:starting:scale-[0.98] open:backdrop:bg-tm01-scrim open:backdrop:backdrop-blur-[5px] motion-reduce:transition-none">
    <form method="dialog" class="group relative p-8">
      <input type="radio" name="tm-01-tab" id="tm-01-in" class="peer/in sr-only" checked>
      <input type="radio" name="tm-01-tab" id="tm-01-up" class="peer/up sr-only">

      <button type="submit" value="close" aria-label="Close dialog" class="absolute top-3.5 right-3.5 grid place-items-center size-9 rounded-lg border-0 bg-transparent text-tm01-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm01-well hover:text-tm01-ink focus-visible:outline-2 focus-visible:outline-tm01-acc focus-visible:outline-offset-2">&times;</button>

      <div class="relative grid grid-cols-2 mt-1 mb-6 p-1 rounded-xl bg-tm01-well border border-tm01-edge">
        <span aria-hidden="true" class="absolute z-[1] inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-lg bg-tm01-pill border border-tm01-pilledge transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[#tm-01-up:checked]:translate-x-full motion-reduce:transition-none"></span>
        <label for="tm-01-in" class="relative z-[2] grid place-items-center min-h-10 rounded-lg text-[13.5px] font-semibold text-tm01-mut cursor-pointer transition-colors duration-200 group-has-[#tm-01-in:checked]:text-white group-has-[#tm-01-in:focus-visible]:outline-2 group-has-[#tm-01-in:focus-visible]:outline-tm01-acc group-has-[#tm-01-in:focus-visible]:outline-offset-2">Sign in</label>
        <label for="tm-01-up" class="relative z-[2] grid place-items-center min-h-10 rounded-lg text-[13.5px] font-semibold text-tm01-mut cursor-pointer transition-colors duration-200 group-has-[#tm-01-up:checked]:text-white group-has-[#tm-01-up:focus-visible]:outline-2 group-has-[#tm-01-up:focus-visible]:outline-tm01-acc group-has-[#tm-01-up:focus-visible]:outline-offset-2">Create account</label>
      </div>

      <h2 id="tm-01-title" class="mb-5 text-[22px] font-bold tracking-[-0.01em] text-tm01-hi">
        <span class="group-has-[#tm-01-up:checked]:hidden">Welcome back</span>
        <span class="hidden group-has-[#tm-01-up:checked]:inline">Create your account</span>
      </h2>

      <div class="relative mb-3.5 hidden group-has-[#tm-01-up:checked]:block">
        <input type="text" id="tm-01-name" placeholder=" " autocomplete="name" class="peer w-full pt-[18px] pb-2 px-3.5 rounded-[10px] border border-tm01-edge bg-tm01-well text-[15px] text-tm01-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm01-acc focus:shadow-[0_0_0_3px_var(--color-tm01-ring)]">
        <label for="tm-01-name" class="absolute left-3.5 top-[15px] origin-left text-[15px] text-tm01-mut pointer-events-none transition-[transform,font-size,color] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[11px] peer-focus:text-tm01-acc peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:text-tm01-acc motion-reduce:transition-none">Full name</label>
      </div>

      <div class="relative mb-3.5">
        <input type="email" id="tm-01-email" placeholder=" " required autocomplete="email" class="peer w-full pt-[18px] pb-2 px-3.5 rounded-[10px] border border-tm01-edge bg-tm01-well text-[15px] text-tm01-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm01-acc focus:shadow-[0_0_0_3px_var(--color-tm01-ring)]">
        <label for="tm-01-email" class="absolute left-3.5 top-[15px] origin-left text-[15px] text-tm01-mut pointer-events-none transition-[transform,font-size,color] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[11px] peer-focus:text-tm01-acc peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:text-tm01-acc motion-reduce:transition-none">Email address</label>
      </div>

      <div class="relative mb-3.5">
        <input type="password" id="tm-01-pass" placeholder=" " required autocomplete="current-password" class="peer w-full pt-[18px] pb-2 px-3.5 rounded-[10px] border border-tm01-edge bg-tm01-well text-[15px] text-tm01-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm01-acc focus:shadow-[0_0_0_3px_var(--color-tm01-ring)]">
        <label for="tm-01-pass" class="absolute left-3.5 top-[15px] origin-left text-[15px] text-tm01-mut pointer-events-none transition-[transform,font-size,color] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[11px] peer-focus:text-tm01-acc peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:text-tm01-acc motion-reduce:transition-none">Password</label>
      </div>

      <button type="button" data-tm01-submit class="w-full min-h-11 mt-2 rounded-[10px] border-0 bg-tm01-acc text-[15px] font-bold text-tm01-onacc cursor-pointer transition-[filter,translate] duration-200 hover:brightness-110 active:translate-y-px focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 motion-reduce:active:translate-y-0">
        <span class="group-has-[#tm-01-up:checked]:hidden">Sign in</span>
        <span class="hidden group-has-[#tm-01-up:checked]:inline">Create account</span>
      </button>
      <p class="mt-4 text-center text-[12.5px] leading-relaxed text-tm01-mut">By continuing you agree to our Terms &amp; Privacy Policy.</p>
    </form>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-tm01-bg: #0b0e14;
  --color-tm01-bg: oklch(0.16 0.015 260);
  --color-tm01-card: #141821;
  --color-tm01-card: oklch(0.21 0.017 260);
  --color-tm01-well: #0c0f16;
  --color-tm01-well: oklch(0.17 0.015 260);
  --color-tm01-edge: #232a36;
  --color-tm01-edge: oklch(0.29 0.017 260);
  --color-tm01-btn: #151b27;
  --color-tm01-btn: oklch(0.22 0.025 260);
  --color-tm01-btnedge: #2a3346;
  --color-tm01-btnedge: oklch(0.32 0.03 260);
  --color-tm01-btnhover: #1c2536;
  --color-tm01-btnhover: oklch(0.26 0.035 260);
  --color-tm01-ink: #e7ecf5;
  --color-tm01-ink: oklch(0.92 0.01 260);
  --color-tm01-hi: #f3f6fc;
  --color-tm01-hi: oklch(0.97 0.006 260);
  --color-tm01-mut: #9aa3b4;
  --color-tm01-mut: oklch(0.72 0.02 260);
  --color-tm01-acc: #7aa2ff;
  --color-tm01-acc: oklch(0.72 0.16 255);
  --color-tm01-onacc: #06101f;
  --color-tm01-onacc: oklch(0.18 0.04 255);
  --color-tm01-pill: #253049;
  --color-tm01-pill: oklch(0.32 0.06 258);
  --color-tm01-pilledge: #3c4f7a;
  --color-tm01-pilledge: oklch(0.44 0.1 258);
  --color-tm01-ring: #7aa2ff4d;
  --color-tm01-ring: oklch(0.72 0.16 255 / 0.3);
  --color-tm01-scrim: #06080ca3;
  --color-tm01-scrim: oklch(0.13 0.01 260 / 0.64);
  --font-tm01-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Root rule: the width half of the full-bleed contract; the height comes from the
   markup's min-h-svh utility. */

.tm-01 {
  width: 100%;
}
/* The one thing the variants cannot hold: the ::backdrop enter/exit transition.
   open:backdrop:* sets the lit state; these four lines animate it in and out. */

#tm-01-dlg::backdrop {
  background: oklch(0.13 0.01 260 / 0);
  backdrop-filter: blur(0px);
  transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}

@starting-style {
  #tm-01-dlg[open]::backdrop {
    background: oklch(0.13 0.01 260 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tm-01-dlg::backdrop {
    transition-duration: .001s;
  }
}
(function () {
  var root = document.querySelector('.tm-01');
  var dlg = root.querySelector('#tm-01-dlg');
  var card = dlg.querySelector('form');
  root.querySelector('[data-tm01-open]').addEventListener('click', function () { dlg.showModal(); });
  root.querySelector('[data-tm01-submit]').addEventListener('click', function () { dlg.close(); });
  // Light-dismiss: the click landed on the dialog box, outside the card's rect.
  dlg.addEventListener('click', function (e) {
    var r = card.getBoundingClientRect();
    if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tm01-bg: #0b0e14;
  --color-tm01-bg: oklch(0.16 0.015 260);
  --color-tm01-card: #141821;
  --color-tm01-card: oklch(0.21 0.017 260);
  --color-tm01-well: #0c0f16;
  --color-tm01-well: oklch(0.17 0.015 260);
  --color-tm01-edge: #232a36;
  --color-tm01-edge: oklch(0.29 0.017 260);
  --color-tm01-btn: #151b27;
  --color-tm01-btn: oklch(0.22 0.025 260);
  --color-tm01-btnedge: #2a3346;
  --color-tm01-btnedge: oklch(0.32 0.03 260);
  --color-tm01-btnhover: #1c2536;
  --color-tm01-btnhover: oklch(0.26 0.035 260);
  --color-tm01-ink: #e7ecf5;
  --color-tm01-ink: oklch(0.92 0.01 260);
  --color-tm01-hi: #f3f6fc;
  --color-tm01-hi: oklch(0.97 0.006 260);
  --color-tm01-mut: #9aa3b4;
  --color-tm01-mut: oklch(0.72 0.02 260);
  --color-tm01-acc: #7aa2ff;
  --color-tm01-acc: oklch(0.72 0.16 255);
  --color-tm01-onacc: #06101f;
  --color-tm01-onacc: oklch(0.18 0.04 255);
  --color-tm01-pill: #253049;
  --color-tm01-pill: oklch(0.32 0.06 258);
  --color-tm01-pilledge: #3c4f7a;
  --color-tm01-pilledge: oklch(0.44 0.1 258);
  --color-tm01-ring: #7aa2ff4d;
  --color-tm01-ring: oklch(0.72 0.16 255 / 0.3);
  --color-tm01-scrim: #06080ca3;
  --color-tm01-scrim: oklch(0.13 0.01 260 / 0.64);
  --font-tm01-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Root rule: the width half of the full-bleed contract; the height comes from the
   markup's min-h-svh utility. */

.tm-01 {
  width: 100%;
}
/* The one thing the variants cannot hold: the ::backdrop enter/exit transition.
   open:backdrop:* sets the lit state; these four lines animate it in and out. */

#tm-01-dlg::backdrop {
  background: oklch(0.13 0.01 260 / 0);
  backdrop-filter: blur(0px);
  transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}

@starting-style {
  #tm-01-dlg[open]::backdrop {
    background: oklch(0.13 0.01 260 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tm-01-dlg::backdrop {
    transition-duration: .001s;
  }
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Three separated CSS blocks (resting, [open], @starting-style) collapse into one class list via open: and open:starting:. Longer to read, but the animation's three states are now adjacent and impossible to update out of sync.
  • transition-discrete is v4's utility for transition-behavior: allow-discrete — the single declaration that makes dialog exit animations possible at all.
  • The transition list names translate and scale, not transform: v4's transform utilities compile to the individual properties, so a transform entry in transition-[…] is dead code. Every dialog in this collection follows the corrected form.
  • The ::backdrop @starting-style stayed in CSS. Everything else about the backdrop — colour and blur — is a backdrop: utility.
  • UI change from the CSS original: the trigger and submit are now min-h-11 (44px) instead of 13px padding, the tab pill gained a visible border so it reads as selected without relying on fill alone, and the close ✕ grew from 34px to 36px with a real focus ring.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Login / Signup Modal with Native Dialog & Starting-Style
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-login-signup-modal-native-dialog-starting-style/

The reference-grade auth overlay: one native <dialog> flips between Sign in and Create account with zero-JS tabs, floating labels and a blurring backdrop. The port's win is open: + open:starting: + transition-discrete — the closed state, the open state and the frame the browser paints before the transition all sit in one class list on the element that animates.
## HTML
```html
<section class="tm-01 w-full min-h-svh grid place-items-center bg-tm01-bg font-tm01-sans px-6 py-10">
  <button type="button" data-tm01-open class="inline-flex items-center justify-center min-h-11 px-7 rounded-xl text-[15px] font-semibold text-tm01-ink bg-tm01-btn border border-tm01-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm01-btnhover hover:border-tm01-acc focus-visible:outline-2 focus-visible:outline-tm01-acc focus-visible:outline-offset-2">Sign in</button>

  <dialog id="tm-01-dlg" aria-labelledby="tm-01-title" class="m-auto w-[min(400px,94vw)] p-0 border-0 rounded-[18px] text-tm01-ink bg-tm01-card shadow-[0_30px_80px_-30px_#000,0_0_0_1px_var(--color-tm01-edge)] opacity-0 translate-y-2.5 scale-[0.98] transition-[opacity,translate,scale,overlay,display] duration-300 ease-out transition-discrete open:opacity-100 open:translate-y-0 open:scale-100 open:starting:opacity-0 open:starting:translate-y-2.5 open:starting:scale-[0.98] open:backdrop:bg-tm01-scrim open:backdrop:backdrop-blur-[5px] motion-reduce:transition-none">
    <form method="dialog" class="group relative p-8">
      <input type="radio" name="tm-01-tab" id="tm-01-in" class="peer/in sr-only" checked>
      <input type="radio" name="tm-01-tab" id="tm-01-up" class="peer/up sr-only">

      <button type="submit" value="close" aria-label="Close dialog" class="absolute top-3.5 right-3.5 grid place-items-center size-9 rounded-lg border-0 bg-transparent text-tm01-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm01-well hover:text-tm01-ink focus-visible:outline-2 focus-visible:outline-tm01-acc focus-visible:outline-offset-2">&times;</button>

      <div class="relative grid grid-cols-2 mt-1 mb-6 p-1 rounded-xl bg-tm01-well border border-tm01-edge">
        <span aria-hidden="true" class="absolute z-[1] inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-lg bg-tm01-pill border border-tm01-pilledge transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-has-[#tm-01-up:checked]:translate-x-full motion-reduce:transition-none"></span>
        <label for="tm-01-in" class="relative z-[2] grid place-items-center min-h-10 rounded-lg text-[13.5px] font-semibold text-tm01-mut cursor-pointer transition-colors duration-200 group-has-[#tm-01-in:checked]:text-white group-has-[#tm-01-in:focus-visible]:outline-2 group-has-[#tm-01-in:focus-visible]:outline-tm01-acc group-has-[#tm-01-in:focus-visible]:outline-offset-2">Sign in</label>
        <label for="tm-01-up" class="relative z-[2] grid place-items-center min-h-10 rounded-lg text-[13.5px] font-semibold text-tm01-mut cursor-pointer transition-colors duration-200 group-has-[#tm-01-up:checked]:text-white group-has-[#tm-01-up:focus-visible]:outline-2 group-has-[#tm-01-up:focus-visible]:outline-tm01-acc group-has-[#tm-01-up:focus-visible]:outline-offset-2">Create account</label>
      </div>

      <h2 id="tm-01-title" class="mb-5 text-[22px] font-bold tracking-[-0.01em] text-tm01-hi">
        <span class="group-has-[#tm-01-up:checked]:hidden">Welcome back</span>
        <span class="hidden group-has-[#tm-01-up:checked]:inline">Create your account</span>
      </h2>

      <div class="relative mb-3.5 hidden group-has-[#tm-01-up:checked]:block">
        <input type="text" id="tm-01-name" placeholder=" " autocomplete="name" class="peer w-full pt-[18px] pb-2 px-3.5 rounded-[10px] border border-tm01-edge bg-tm01-well text-[15px] text-tm01-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm01-acc focus:shadow-[0_0_0_3px_var(--color-tm01-ring)]">
        <label for="tm-01-name" class="absolute left-3.5 top-[15px] origin-left text-[15px] text-tm01-mut pointer-events-none transition-[transform,font-size,color] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[11px] peer-focus:text-tm01-acc peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:text-tm01-acc motion-reduce:transition-none">Full name</label>
      </div>

      <div class="relative mb-3.5">
        <input type="email" id="tm-01-email" placeholder=" " required autocomplete="email" class="peer w-full pt-[18px] pb-2 px-3.5 rounded-[10px] border border-tm01-edge bg-tm01-well text-[15px] text-tm01-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm01-acc focus:shadow-[0_0_0_3px_var(--color-tm01-ring)]">
        <label for="tm-01-email" class="absolute left-3.5 top-[15px] origin-left text-[15px] text-tm01-mut pointer-events-none transition-[transform,font-size,color] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[11px] peer-focus:text-tm01-acc peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:text-tm01-acc motion-reduce:transition-none">Email address</label>
      </div>

      <div class="relative mb-3.5">
        <input type="password" id="tm-01-pass" placeholder=" " required autocomplete="current-password" class="peer w-full pt-[18px] pb-2 px-3.5 rounded-[10px] border border-tm01-edge bg-tm01-well text-[15px] text-tm01-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm01-acc focus:shadow-[0_0_0_3px_var(--color-tm01-ring)]">
        <label for="tm-01-pass" class="absolute left-3.5 top-[15px] origin-left text-[15px] text-tm01-mut pointer-events-none transition-[transform,font-size,color] duration-200 peer-focus:-translate-y-2.5 peer-focus:text-[11px] peer-focus:text-tm01-acc peer-[:not(:placeholder-shown)]:-translate-y-2.5 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:text-tm01-acc motion-reduce:transition-none">Password</label>
      </div>

      <button type="button" data-tm01-submit class="w-full min-h-11 mt-2 rounded-[10px] border-0 bg-tm01-acc text-[15px] font-bold text-tm01-onacc cursor-pointer transition-[filter,translate] duration-200 hover:brightness-110 active:translate-y-px focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 motion-reduce:active:translate-y-0">
        <span class="group-has-[#tm-01-up:checked]:hidden">Sign in</span>
        <span class="hidden group-has-[#tm-01-up:checked]:inline">Create account</span>
      </button>
      <p class="mt-4 text-center text-[12.5px] leading-relaxed text-tm01-mut">By continuing you agree to our Terms &amp; Privacy Policy.</p>
    </form>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tm01-bg: #0b0e14;
  --color-tm01-bg: oklch(0.16 0.015 260);
  --color-tm01-card: #141821;
  --color-tm01-card: oklch(0.21 0.017 260);
  --color-tm01-well: #0c0f16;
  --color-tm01-well: oklch(0.17 0.015 260);
  --color-tm01-edge: #232a36;
  --color-tm01-edge: oklch(0.29 0.017 260);
  --color-tm01-btn: #151b27;
  --color-tm01-btn: oklch(0.22 0.025 260);
  --color-tm01-btnedge: #2a3346;
  --color-tm01-btnedge: oklch(0.32 0.03 260);
  --color-tm01-btnhover: #1c2536;
  --color-tm01-btnhover: oklch(0.26 0.035 260);
  --color-tm01-ink: #e7ecf5;
  --color-tm01-ink: oklch(0.92 0.01 260);
  --color-tm01-hi: #f3f6fc;
  --color-tm01-hi: oklch(0.97 0.006 260);
  --color-tm01-mut: #9aa3b4;
  --color-tm01-mut: oklch(0.72 0.02 260);
  --color-tm01-acc: #7aa2ff;
  --color-tm01-acc: oklch(0.72 0.16 255);
  --color-tm01-onacc: #06101f;
  --color-tm01-onacc: oklch(0.18 0.04 255);
  --color-tm01-pill: #253049;
  --color-tm01-pill: oklch(0.32 0.06 258);
  --color-tm01-pilledge: #3c4f7a;
  --color-tm01-pilledge: oklch(0.44 0.1 258);
  --color-tm01-ring: #7aa2ff4d;
  --color-tm01-ring: oklch(0.72 0.16 255 / 0.3);
  --color-tm01-scrim: #06080ca3;
  --color-tm01-scrim: oklch(0.13 0.01 260 / 0.64);
  --font-tm01-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Root rule: the width half of the full-bleed contract; the height comes from the
   markup's min-h-svh utility. */

.tm-01 {
  width: 100%;
}
/* The one thing the variants cannot hold: the ::backdrop enter/exit transition.
   open:backdrop:* sets the lit state; these four lines animate it in and out. */

#tm-01-dlg::backdrop {
  background: oklch(0.13 0.01 260 / 0);
  backdrop-filter: blur(0px);
  transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}

@starting-style {
  #tm-01-dlg[open]::backdrop {
    background: oklch(0.13 0.01 260 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tm-01-dlg::backdrop {
    transition-duration: .001s;
  }
}
```

## JavaScript
```js
(function () {
  var root = document.querySelector('.tm-01');
  var dlg = root.querySelector('#tm-01-dlg');
  var card = dlg.querySelector('form');
  root.querySelector('[data-tm01-open]').addEventListener('click', function () { dlg.showModal(); });
  root.querySelector('[data-tm01-submit]').addEventListener('click', function () { dlg.close(); });
  // Light-dismiss: the click landed on the dialog box, outside the card's rect.
  dlg.addEventListener('click', function (e) {
    var r = card.getBoundingClientRect();
    if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
  });
})();
```

How this works

The dialog owns its own animation. The CSS version needed three blocks far apart: the resting rule, .dlg[open], and a top-level @starting-style. Tailwind states all three on the element:

class="opacity-0 translate-y-2.5 scale-[0.98] transition-discrete
       transition-[opacity,translate,scale,overlay,display]
       open:opacity-100 open:translate-y-0 open:scale-100
       open:starting:opacity-0 open:starting:translate-y-2.5"

Read top to bottom that is: closed, open, and the instant before open. Delete the starting: line and the modal snaps in — the failure mode is legible because the cause is adjacent.

The tab switch stays zero-JS. Two sr-only radios sit at the top of the form; group-has-[#tm-01-up:checked]:translate-x-full slides the accent pill, and group-has-[#tm-01-up:checked]: on the form reveals the name field and swaps the heading. No state in JS means the dialog can be server-rendered open.

showModal() still does the hard part. Focus trap, Esc, background inert and focus restore are the browser's job; the eleven lines of JS only open it, close it, and light-dismiss on a backdrop click.

Make it yours

  • Retheme from one token: --color-tm01-acc drives the pill, the focus ring, the submit button and the floated label.
  • Open straight to sign-up by moving checked to #tm-01-up — no other change.
  • Add an OAuth row above the fields: a grid grid-cols-2 gap-2 of buttons plus a before:/after: rule divider.
  • Wire real auth by POSTing on submit; keep method="dialog" as the no-JS fallback.

Gotchas — read before shipping

  • showModal(), never show() — show() opens a non-modal dialog with no backdrop, no focus trap and no ::backdrop for your backdrop: utilities to style.
  • transition-discrete is not optional decoration: without it display flips instantly and the exit animation never renders, no matter how many duration-* utilities are present.
  • v4's translate-* and scale-* utilities emit the standalone translate and scale CSS properties — computed transform is none. Listing transform in the transition property list therefore animates nothing and the dialog pops instead of sliding: name translate and scale explicitly. This is the single most common v3→v4 animation regression, and it fails silently.
  • The floating label depends on placeholder=" " (one space) so :placeholder-shown can detect emptiness — deleting the placeholder silently kills the label motion.
  • The ::backdrop fade keeps its @starting-style in the stylesheet. open:starting:backdrop: nests three variants around a pseudo-element the browser only creates on open, and the emitted order is not stable across builds — four lines of CSS are the honest answer.

Browser support

ChromeSafariFirefoxEdge
111+ · 117+ anim16.4+ · 17.5+ anim128+ · 129+ anim111+ · 117+ anim

<dialog>/showModal() is Baseline; @starting-style and transition-behavior are the newer gate (Chrome 117, Safari 17.5, Firefox 129) and degrade to an instant show, which never blocks the flow.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

HTML & JSno Tailwind involved

Search CodeFronts

Loading…