36 Tailwind CSS Modals
25 / 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 GDPR Cookie Preferences Modal with Unfolding Category Rows

A consent centre whose four category rows unfold downward like origami while the title and the three actions are readable from frame one — the fast path stays fast. The port drives the cascade from the dialog's own [open] state with in-[dialog[open]]: plus per-row [transition-delay:…], and builds the toggles from peer-checked: instead of custom controls.

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="tw-07 w-full min-h-svh grid place-items-center bg-tw07-bg font-tw07-sans px-6 py-10">
  <button type="button" data-tw07-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw07-line bg-tw07-card text-[15px] font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well2 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Cookie preferences</button>

  <dialog id="tw-07-dlg" aria-labelledby="tw-07-title" class="m-auto w-[min(560px,94vw)] p-0 border-0 rounded-[14px] text-left text-tw07-ink bg-tw07-card shadow-[0_40px_60px_-20px_#0f172a59] opacity-0 transition-[opacity,overlay,display] duration-200 transition-discrete open:opacity-100 open:starting:opacity-0 open:backdrop:bg-tw07-scrim open:backdrop:backdrop-blur-[4px]">
    <div class="relative px-8 pt-8 pb-6">
      <button type="button" data-tw07-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-tw07-sub text-base cursor-pointer transition-colors duration-200 hover:bg-tw07-well2 hover:text-tw07-ink focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">&#10005;</button>

      <h2 id="tw-07-title" class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Cookie preferences</h2>
      <p class="mb-5 text-sm leading-relaxed text-tw07-sub text-pretty">We use cookies to enhance your experience, analyze site traffic, and improve services. You can accept all, reject all non-essential, or customize per category.</p>

      <div class="flex flex-col gap-2 mb-5 perspective-[900px]">
        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:0ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Strictly necessary</h3>
              <p class="text-xs leading-snug text-tw07-sub">Session cookies, CSRF tokens, load balancing. Cannot be disabled.</p>
            </div>
            <span class="shrink-0 grid place-items-center w-[42px] h-[22px] rounded-full bg-tw07-locked text-[0.62rem] font-extrabold tracking-wider text-white">ON</span>
          </div>
        </div>

        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:180ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Analytics</h3>
              <p class="text-xs leading-snug text-tw07-sub">Google Analytics, PostHog. Helps us understand aggregate usage patterns.</p>
            </div>
            <label class="shrink-0 cursor-pointer">
              <input type="checkbox" checked class="peer sr-only">
              <span class="sr-only">Allow analytics cookies</span>
              <span aria-hidden="true" class="grid items-center w-[42px] h-[22px] rounded-full bg-tw07-off transition-colors duration-200 peer-checked:bg-tw07-good peer-focus-visible:outline-2 peer-focus-visible:outline-tw07-ink peer-focus-visible:outline-offset-2"><span class="block size-[18px] ml-0.5 rounded-full bg-white transition-transform duration-200 peer-checked:translate-x-5 motion-reduce:transition-none"></span></span>
            </label>
          </div>
        </div>

        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:360ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Marketing</h3>
              <p class="text-xs leading-snug text-tw07-sub">Facebook Pixel, LinkedIn Insight Tag. Enables retargeting and personalized ads.</p>
            </div>
            <label class="shrink-0 cursor-pointer">
              <input type="checkbox" class="peer sr-only">
              <span class="sr-only">Allow marketing cookies</span>
              <span aria-hidden="true" class="grid items-center w-[42px] h-[22px] rounded-full bg-tw07-off transition-colors duration-200 peer-checked:bg-tw07-good peer-focus-visible:outline-2 peer-focus-visible:outline-tw07-ink peer-focus-visible:outline-offset-2"><span class="block size-[18px] ml-0.5 rounded-full bg-white transition-transform duration-200 peer-checked:translate-x-5 motion-reduce:transition-none"></span></span>
            </label>
          </div>
        </div>

        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:540ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Personalization</h3>
              <p class="text-xs leading-snug text-tw07-sub">Recommendation engine, saved preferences, theme mode. Improves your experience.</p>
            </div>
            <label class="shrink-0 cursor-pointer">
              <input type="checkbox" checked class="peer sr-only">
              <span class="sr-only">Allow personalization cookies</span>
              <span aria-hidden="true" class="grid items-center w-[42px] h-[22px] rounded-full bg-tw07-off transition-colors duration-200 peer-checked:bg-tw07-good peer-focus-visible:outline-2 peer-focus-visible:outline-tw07-ink peer-focus-visible:outline-offset-2"><span class="block size-[18px] ml-0.5 rounded-full bg-white transition-transform duration-200 peer-checked:translate-x-5 motion-reduce:transition-none"></span></span>
            </label>
          </div>
        </div>
      </div>

      <div class="grid grid-cols-3 gap-2 mb-4 max-[440px]:grid-cols-1">
        <button type="button" autofocus data-tw07-close class="min-h-11 px-4 rounded-lg border border-tw07-line bg-tw07-well2 text-sm font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well3 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Reject all</button>
        <button type="button" data-tw07-close class="min-h-11 px-4 rounded-lg border border-tw07-line bg-tw07-well2 text-sm font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well3 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Save preferences</button>
        <button type="button" data-tw07-close class="min-h-11 px-4 rounded-lg border border-tw07-line bg-tw07-well2 text-sm font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well3 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Accept all</button>
      </div>

      <p class="pt-3.5 border-t border-tw07-line text-xs text-tw07-sub">Read our <a href="#" class="text-tw07-ink underline">Cookie Policy</a> and <a href="#" class="text-tw07-ink underline">Privacy Policy</a>. You can change preferences anytime.</p>
    </div>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-tw07-bg: #f8fafc;
  --color-tw07-bg: oklch(0.98 0.005 250);
  --color-tw07-card: #ffffff;
  --color-tw07-card: oklch(1 0 0);
  --color-tw07-well: #fafbfc;
  --color-tw07-well: oklch(0.98 0.003 255);
  --color-tw07-well2: #f1f5f9;
  --color-tw07-well2: oklch(0.96 0.006 250);
  --color-tw07-well3: #e2e8f0;
  --color-tw07-well3: oklch(0.92 0.01 250);
  --color-tw07-ink: #1e293b;
  --color-tw07-ink: oklch(0.3 0.03 258);
  --color-tw07-sub: #55657c;
  --color-tw07-sub: oklch(0.5 0.03 255);
  --color-tw07-line: #e2e8f0;
  --color-tw07-line: oklch(0.92 0.01 250);
  --color-tw07-good: #059669;
  --color-tw07-good: oklch(0.63 0.14 163);
  --color-tw07-off: #94a3b8;
  --color-tw07-off: oklch(0.7 0.03 255);
  --color-tw07-locked: #64748b;
  --color-tw07-locked: oklch(0.55 0.03 255);
  --color-tw07-scrim: #0f172a99;
  --color-tw07-scrim: oklch(0.23 0.04 265 / 0.6);
  --font-tw07-sans: 'Outfit', system-ui, sans-serif;
}

.tw-07 {
  width: 100%;
}

#tw-07-dlg::backdrop {
  background: oklch(0.23 0.04 265 / 0);
  backdrop-filter: blur(0px);
  transition: background .2s ease, backdrop-filter .2s ease, overlay .2s allow-discrete, display .2s allow-discrete;
}

@starting-style {
  #tw-07-dlg[open]::backdrop {
    background: oklch(0.23 0.04 265 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tw-07-dlg::backdrop {
    transition-duration: .001s;
  }
}
(function () {
  var root = document.querySelector('.tw-07');
  var dlg = root.querySelector('#tw-07-dlg');
  var card = dlg.firstElementChild;
  root.querySelector('[data-tw07-open]').addEventListener('click', function () { dlg.showModal(); });
  root.querySelectorAll('[data-tw07-close]').forEach(function (b) {
    // Production: read the toggles here and gate your script loader on the result.
    b.addEventListener('click', function () { dlg.close(); });
  });
  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-tw07-bg: #f8fafc;
  --color-tw07-bg: oklch(0.98 0.005 250);
  --color-tw07-card: #ffffff;
  --color-tw07-card: oklch(1 0 0);
  --color-tw07-well: #fafbfc;
  --color-tw07-well: oklch(0.98 0.003 255);
  --color-tw07-well2: #f1f5f9;
  --color-tw07-well2: oklch(0.96 0.006 250);
  --color-tw07-well3: #e2e8f0;
  --color-tw07-well3: oklch(0.92 0.01 250);
  --color-tw07-ink: #1e293b;
  --color-tw07-ink: oklch(0.3 0.03 258);
  --color-tw07-sub: #55657c;
  --color-tw07-sub: oklch(0.5 0.03 255);
  --color-tw07-line: #e2e8f0;
  --color-tw07-line: oklch(0.92 0.01 250);
  --color-tw07-good: #059669;
  --color-tw07-good: oklch(0.63 0.14 163);
  --color-tw07-off: #94a3b8;
  --color-tw07-off: oklch(0.7 0.03 255);
  --color-tw07-locked: #64748b;
  --color-tw07-locked: oklch(0.55 0.03 255);
  --color-tw07-scrim: #0f172a99;
  --color-tw07-scrim: oklch(0.23 0.04 265 / 0.6);
  --font-tw07-sans: 'Outfit', system-ui, sans-serif;
}

.tw-07 {
  width: 100%;
}

#tw-07-dlg::backdrop {
  background: oklch(0.23 0.04 265 / 0);
  backdrop-filter: blur(0px);
  transition: background .2s ease, backdrop-filter .2s ease, overlay .2s allow-discrete, display .2s allow-discrete;
}

@starting-style {
  #tw-07-dlg[open]::backdrop {
    background: oklch(0.23 0.04 265 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tw-07-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=Outfit:wght@400;500;600;700;800&display=swap">

What changed in the port

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

  • The original needed an .is-unfolding class added on the next animation frame, plus a comment explaining the race. Reading in-[dialog[open]]: removes both: the resting state exists from page load and the open state is the browser's own attribute.
  • Four nth-child delay rules become four [transition-delay:…] utilities on the rows they delay — insert a fifth category and its timing travels with it.
  • The toggles are sr-only checkboxes with a peer-checked: track. Native control, native keyboard, no ARIA to keep in sync, and an sr-only label so each switch says which category it governs.
  • The three actions share one grid track and one style. Equal weight is the structure, not a promise in a comment.
  • UI change from the CSS original: all three actions are 44px and wrap to a single column under 440px, and the locked Necessary pill uses a slate fill so it does not read as an active green toggle.
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 GDPR Cookie Preferences Modal with Unfolding Category Rows
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-gdpr-cookie-preferences-modal-unfolding-rows/

A consent centre whose four category rows unfold downward like origami while the title and the three actions are readable from frame one — the fast path stays fast. The port drives the cascade from the dialog's own [open] state with in-[dialog[open]]: plus per-row [transition-delay:…], and builds the toggles from peer-checked: instead of custom controls.
## HTML
```html
<section class="tw-07 w-full min-h-svh grid place-items-center bg-tw07-bg font-tw07-sans px-6 py-10">
  <button type="button" data-tw07-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw07-line bg-tw07-card text-[15px] font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well2 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Cookie preferences</button>

  <dialog id="tw-07-dlg" aria-labelledby="tw-07-title" class="m-auto w-[min(560px,94vw)] p-0 border-0 rounded-[14px] text-left text-tw07-ink bg-tw07-card shadow-[0_40px_60px_-20px_#0f172a59] opacity-0 transition-[opacity,overlay,display] duration-200 transition-discrete open:opacity-100 open:starting:opacity-0 open:backdrop:bg-tw07-scrim open:backdrop:backdrop-blur-[4px]">
    <div class="relative px-8 pt-8 pb-6">
      <button type="button" data-tw07-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-tw07-sub text-base cursor-pointer transition-colors duration-200 hover:bg-tw07-well2 hover:text-tw07-ink focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">&#10005;</button>

      <h2 id="tw-07-title" class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Cookie preferences</h2>
      <p class="mb-5 text-sm leading-relaxed text-tw07-sub text-pretty">We use cookies to enhance your experience, analyze site traffic, and improve services. You can accept all, reject all non-essential, or customize per category.</p>

      <div class="flex flex-col gap-2 mb-5 perspective-[900px]">
        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:0ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Strictly necessary</h3>
              <p class="text-xs leading-snug text-tw07-sub">Session cookies, CSRF tokens, load balancing. Cannot be disabled.</p>
            </div>
            <span class="shrink-0 grid place-items-center w-[42px] h-[22px] rounded-full bg-tw07-locked text-[0.62rem] font-extrabold tracking-wider text-white">ON</span>
          </div>
        </div>

        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:180ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Analytics</h3>
              <p class="text-xs leading-snug text-tw07-sub">Google Analytics, PostHog. Helps us understand aggregate usage patterns.</p>
            </div>
            <label class="shrink-0 cursor-pointer">
              <input type="checkbox" checked class="peer sr-only">
              <span class="sr-only">Allow analytics cookies</span>
              <span aria-hidden="true" class="grid items-center w-[42px] h-[22px] rounded-full bg-tw07-off transition-colors duration-200 peer-checked:bg-tw07-good peer-focus-visible:outline-2 peer-focus-visible:outline-tw07-ink peer-focus-visible:outline-offset-2"><span class="block size-[18px] ml-0.5 rounded-full bg-white transition-transform duration-200 peer-checked:translate-x-5 motion-reduce:transition-none"></span></span>
            </label>
          </div>
        </div>

        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:360ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Marketing</h3>
              <p class="text-xs leading-snug text-tw07-sub">Facebook Pixel, LinkedIn Insight Tag. Enables retargeting and personalized ads.</p>
            </div>
            <label class="shrink-0 cursor-pointer">
              <input type="checkbox" class="peer sr-only">
              <span class="sr-only">Allow marketing cookies</span>
              <span aria-hidden="true" class="grid items-center w-[42px] h-[22px] rounded-full bg-tw07-off transition-colors duration-200 peer-checked:bg-tw07-good peer-focus-visible:outline-2 peer-focus-visible:outline-tw07-ink peer-focus-visible:outline-offset-2"><span class="block size-[18px] ml-0.5 rounded-full bg-white transition-transform duration-200 peer-checked:translate-x-5 motion-reduce:transition-none"></span></span>
            </label>
          </div>
        </div>

        <div class="px-4 py-3.5 rounded-[9px] border border-tw07-line bg-tw07-well origin-bottom backface-hidden opacity-0 rotate-x-[-180deg] transition-[opacity,rotate] duration-700 ease-[cubic-bezier(.4,.15,.25,1)] in-[dialog[open]]:opacity-100 in-[dialog[open]]:rotate-x-0 [transition-delay:540ms] motion-reduce:transition-opacity motion-reduce:rotate-x-0 motion-reduce:[transition-delay:0ms]">
          <div class="flex justify-between items-start gap-3.5">
            <div>
              <h3 class="mb-0.5 text-sm font-bold">Personalization</h3>
              <p class="text-xs leading-snug text-tw07-sub">Recommendation engine, saved preferences, theme mode. Improves your experience.</p>
            </div>
            <label class="shrink-0 cursor-pointer">
              <input type="checkbox" checked class="peer sr-only">
              <span class="sr-only">Allow personalization cookies</span>
              <span aria-hidden="true" class="grid items-center w-[42px] h-[22px] rounded-full bg-tw07-off transition-colors duration-200 peer-checked:bg-tw07-good peer-focus-visible:outline-2 peer-focus-visible:outline-tw07-ink peer-focus-visible:outline-offset-2"><span class="block size-[18px] ml-0.5 rounded-full bg-white transition-transform duration-200 peer-checked:translate-x-5 motion-reduce:transition-none"></span></span>
            </label>
          </div>
        </div>
      </div>

      <div class="grid grid-cols-3 gap-2 mb-4 max-[440px]:grid-cols-1">
        <button type="button" autofocus data-tw07-close class="min-h-11 px-4 rounded-lg border border-tw07-line bg-tw07-well2 text-sm font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well3 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Reject all</button>
        <button type="button" data-tw07-close class="min-h-11 px-4 rounded-lg border border-tw07-line bg-tw07-well2 text-sm font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well3 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Save preferences</button>
        <button type="button" data-tw07-close class="min-h-11 px-4 rounded-lg border border-tw07-line bg-tw07-well2 text-sm font-semibold text-tw07-ink cursor-pointer transition-colors duration-200 hover:bg-tw07-well3 focus-visible:outline-2 focus-visible:outline-tw07-ink focus-visible:outline-offset-2">Accept all</button>
      </div>

      <p class="pt-3.5 border-t border-tw07-line text-xs text-tw07-sub">Read our <a href="#" class="text-tw07-ink underline">Cookie Policy</a> and <a href="#" class="text-tw07-ink underline">Privacy Policy</a>. You can change preferences anytime.</p>
    </div>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tw07-bg: #f8fafc;
  --color-tw07-bg: oklch(0.98 0.005 250);
  --color-tw07-card: #ffffff;
  --color-tw07-card: oklch(1 0 0);
  --color-tw07-well: #fafbfc;
  --color-tw07-well: oklch(0.98 0.003 255);
  --color-tw07-well2: #f1f5f9;
  --color-tw07-well2: oklch(0.96 0.006 250);
  --color-tw07-well3: #e2e8f0;
  --color-tw07-well3: oklch(0.92 0.01 250);
  --color-tw07-ink: #1e293b;
  --color-tw07-ink: oklch(0.3 0.03 258);
  --color-tw07-sub: #55657c;
  --color-tw07-sub: oklch(0.5 0.03 255);
  --color-tw07-line: #e2e8f0;
  --color-tw07-line: oklch(0.92 0.01 250);
  --color-tw07-good: #059669;
  --color-tw07-good: oklch(0.63 0.14 163);
  --color-tw07-off: #94a3b8;
  --color-tw07-off: oklch(0.7 0.03 255);
  --color-tw07-locked: #64748b;
  --color-tw07-locked: oklch(0.55 0.03 255);
  --color-tw07-scrim: #0f172a99;
  --color-tw07-scrim: oklch(0.23 0.04 265 / 0.6);
  --font-tw07-sans: 'Outfit', system-ui, sans-serif;
}

.tw-07 {
  width: 100%;
}

#tw-07-dlg::backdrop {
  background: oklch(0.23 0.04 265 / 0);
  backdrop-filter: blur(0px);
  transition: background .2s ease, backdrop-filter .2s ease, overlay .2s allow-discrete, display .2s allow-discrete;
}

@starting-style {
  #tw-07-dlg[open]::backdrop {
    background: oklch(0.23 0.04 265 / 0);
    backdrop-filter: blur(0px);
  }
}

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

## JavaScript
```js
(function () {
  var root = document.querySelector('.tw-07');
  var dlg = root.querySelector('#tw-07-dlg');
  var card = dlg.firstElementChild;
  root.querySelector('[data-tw07-open]').addEventListener('click', function () { dlg.showModal(); });
  root.querySelectorAll('[data-tw07-close]').forEach(function (b) {
    // Production: read the toggles here and gate your script loader on the result.
    b.addEventListener('click', function () { dlg.close(); });
  });
  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

Actions first, theatre second. The header and the three buttons carry no entrance at all; only the category rows animate. A visitor who wants Reject all can click it in the first frame, which is both the kind thing and the compliant thing.

The cascade reads the dialog. Each row transitions from rotate-x-[-180deg] to flat when an ancestor dialog is [open], so there is no second state class to add on the next frame and no rAF race to get wrong.

Equal weight is enforced by the grid. Accept all, Reject all and Save preferences share one grid-cols-3 track with the same sizing; the only difference is fill, and it is not applied to Accept.

Make it yours

  • For CCPA, replace Reject all with the verbatim "Do Not Sell or Share My Personal Info" string — synonyms do not satisfy the statute.
  • Pre-decline analytics and marketing when navigator.globalPrivacyControl is true, before the dialog is shown.
  • Replace the generic categories with your cookie audit, one row per category with its retention period.
  • Re-prompt when the stored consent timestamp is older than twelve months.

Gotchas — read before shipping

  • A banner is not compliance. Non-essential scripts must not run until the visitor has chosen; a preferences UI in front of already-firing analytics is the most common GDPR failure.
  • Accept and Reject must look the same. Regulators have fined exactly the design where Accept is filled and Reject is a faint link.
  • The Necessary row is immutable — a locked state, not an unchecked toggle the visitor can flip.
  • The unfold needs perspective-* on the ancestor. Without it rotate-x collapses to a flat squash and the metaphor disappears.
  • Never delay the primary actions behind the cascade. Decorative motion in front of a legal choice is a dark pattern by another name.

Browser support

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

3D transforms are Baseline 2016; only the dialog's @starting-style fade is on the newer gate and degrades to an instant show.

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…