36 Tailwind CSS Modals
35 / 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 Terms of Service Modal with Letter-Slide and Consent Gate

Post-signup legal consent with the collection's most restrained motion: a 20px slide down and a fade, ease-out, no overshoot — a document placed on a desk. The gate is the substance: Accept stays disabled until the agreement box is ticked, driven by has-[#tw-17-agree:checked]:, with marketing consent kept separate.

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-17 w-full min-h-svh grid place-items-center bg-tw17-bg font-tw17-sans px-6 py-10">
  <button type="button" data-tw17-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-[9px] border-0 bg-tw17-ink text-[15px] font-semibold text-white cursor-pointer transition-colors duration-200 hover:bg-tw17-inkhover focus-visible:outline-2 focus-visible:outline-tw17-ink focus-visible:outline-offset-2">Complete signup</button>

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

      <p class="inline-block mb-3.5 px-3 py-1.5 rounded-full bg-tw17-well2 font-tw17-mono text-xs font-bold text-tw17-sub">Terms v3.2 · Updated 2026-07-15</p>
      <h2 id="tw-17-title" class="mb-2 text-[1.3rem] font-extrabold tracking-[-0.015em] leading-tight">Terms of Service &amp; Privacy Policy</h2>
      <p class="mb-[18px] text-sm leading-relaxed text-tw17-sub">Please review before continuing. Your acceptance is required to use Northlight.</p>

      <div tabindex="0" aria-label="Terms of Service, scroll to read in full" class="max-h-[230px] overflow-y-auto overscroll-contain mb-[18px] px-[18px] py-4 rounded-[9px] border border-tw17-line bg-tw17-well focus-visible:outline-2 focus-visible:outline-tw17-ink focus-visible:outline-offset-2">
        <h3 class="mb-1.5 text-[0.85rem] font-bold">1. Acceptance of terms</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">By accessing Northlight ("the Service"), you agree to be bound by these Terms of Service and our Privacy Policy. If you disagree with any part, do not use the Service.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">2. Your account</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. You must notify us immediately of any unauthorized use.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">3. Data collection &amp; use</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">We collect and process personal data as described in our <a href="#" class="font-semibold text-tw17-ink underline">Privacy Policy</a>. This includes account details, usage analytics, and content you create. We do not sell your data to third parties.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">4. Content ownership</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">You retain ownership of content you create in the Service. By using the Service, you grant Northlight a limited license to store, process, and display your content for the purpose of providing the Service.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">5. Termination</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">You may terminate your account at any time. We may suspend or terminate accounts that violate these Terms. Upon termination, we will delete your data within 30 days per GDPR Article 17.</p>
      </div>

      <div class="group/gate">
        <label class="flex items-start gap-2.5 mb-2.5 py-1 text-[13.5px] leading-relaxed cursor-pointer">
          <input type="checkbox" id="tw-17-agree" required class="shrink-0 mt-0.5 size-4 accent-tw17-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw17-good focus-visible:outline-offset-2">
          <span>I have read and agree to the <a href="#" class="font-semibold text-tw17-ink underline">Terms of Service</a> and <a href="#" class="font-semibold text-tw17-ink underline">Privacy Policy</a></span>
        </label>
        <label class="flex items-start gap-2.5 mb-2.5 py-1 text-[13.5px] leading-relaxed cursor-pointer">
          <input type="checkbox" class="shrink-0 mt-0.5 size-4 accent-tw17-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw17-good focus-visible:outline-offset-2">
          <span>Send me product updates and newsletter (separate, optional consent)</span>
        </label>

        <div class="grid grid-cols-2 gap-2.5 mt-3.5">
          <button type="button" data-tw17-close class="min-h-11 rounded-[9px] border-0 bg-tw17-well2 text-sm font-bold text-tw17-ink cursor-pointer transition-colors duration-200 hover:bg-tw17-well3 focus-visible:outline-2 focus-visible:outline-tw17-ink focus-visible:outline-offset-2">Decline</button>
          <button type="button" data-tw17-accept data-tw17-close disabled class="min-h-11 rounded-[9px] border-0 bg-tw17-good text-sm font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-tw17-goodhover focus-visible:outline-2 focus-visible:outline-tw17-good focus-visible:outline-offset-2 disabled:bg-tw17-well3 disabled:text-tw17-sub disabled:cursor-not-allowed">Accept &amp; continue</button>
        </div>
        <p class="mt-2.5 text-xs text-tw17-sub group-has-[#tw-17-agree:checked]/gate:invisible">Tick the agreement box to enable Accept.</p>
      </div>
    </div>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-tw17-bg: #f8fafc;
  --color-tw17-bg: oklch(0.98 0.005 250);
  --color-tw17-card: #ffffff;
  --color-tw17-card: oklch(1 0 0);
  --color-tw17-well: #fafbfc;
  --color-tw17-well: oklch(0.98 0.003 255);
  --color-tw17-well2: #f1f5f9;
  --color-tw17-well2: oklch(0.96 0.006 250);
  --color-tw17-well3: #e2e8f0;
  --color-tw17-well3: oklch(0.92 0.01 250);
  --color-tw17-ink: #0f172a;
  --color-tw17-ink: oklch(0.23 0.04 265);
  --color-tw17-inkhover: #1e293b;
  --color-tw17-inkhover: oklch(0.3 0.03 258);
  --color-tw17-sub: #52627a;
  --color-tw17-sub: oklch(0.48 0.03 255);
  --color-tw17-line: #e2e8f0;
  --color-tw17-line: oklch(0.92 0.01 250);
  --color-tw17-good: #047857;
  --color-tw17-good: oklch(0.52 0.12 164);
  --color-tw17-goodhover: #065f46;
  --color-tw17-goodhover: oklch(0.45 0.1 164);
  --color-tw17-scrim: #0f172a8c;
  --color-tw17-scrim: oklch(0.23 0.04 265 / 0.55);
  --font-tw17-sans: 'Outfit', system-ui, sans-serif;
  --font-tw17-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tw-17 {
  width: 100%;
}

#tw-17-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-17-dlg[open]::backdrop {
    background: oklch(0.23 0.04 265 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tw-17-dlg::backdrop {
    transition-duration: .001s;
  }
}
(function () {
  var root = document.querySelector('.tw-17');
  var dlg = root.querySelector('#tw-17-dlg');
  var card = dlg.firstElementChild;
  var agree = root.querySelector('#tw-17-agree');
  var accept = root.querySelector('[data-tw17-accept]');

  agree.addEventListener('change', function () { accept.disabled = !agree.checked; });

  root.querySelector('[data-tw17-open]').addEventListener('click', function () {
    dlg.showModal();
    dlg.querySelector('[tabindex="0"]').focus();
  });
  root.querySelectorAll('[data-tw17-close]').forEach(function (b) {
    // Production: log the accepted version, timestamp, IP and user agent here.
    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-tw17-bg: #f8fafc;
  --color-tw17-bg: oklch(0.98 0.005 250);
  --color-tw17-card: #ffffff;
  --color-tw17-card: oklch(1 0 0);
  --color-tw17-well: #fafbfc;
  --color-tw17-well: oklch(0.98 0.003 255);
  --color-tw17-well2: #f1f5f9;
  --color-tw17-well2: oklch(0.96 0.006 250);
  --color-tw17-well3: #e2e8f0;
  --color-tw17-well3: oklch(0.92 0.01 250);
  --color-tw17-ink: #0f172a;
  --color-tw17-ink: oklch(0.23 0.04 265);
  --color-tw17-inkhover: #1e293b;
  --color-tw17-inkhover: oklch(0.3 0.03 258);
  --color-tw17-sub: #52627a;
  --color-tw17-sub: oklch(0.48 0.03 255);
  --color-tw17-line: #e2e8f0;
  --color-tw17-line: oklch(0.92 0.01 250);
  --color-tw17-good: #047857;
  --color-tw17-good: oklch(0.52 0.12 164);
  --color-tw17-goodhover: #065f46;
  --color-tw17-goodhover: oklch(0.45 0.1 164);
  --color-tw17-scrim: #0f172a8c;
  --color-tw17-scrim: oklch(0.23 0.04 265 / 0.55);
  --font-tw17-sans: 'Outfit', system-ui, sans-serif;
  --font-tw17-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tw-17 {
  width: 100%;
}

#tw-17-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-17-dlg[open]::backdrop {
    background: oklch(0.23 0.04 265 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tw-17-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&family=JetBrains+Mono:wght@600;700&display=swap">

What changed in the port

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

  • One translate pair and one opacity pair: the whole entrance, stated as open: and open:starting: with no keyframe anywhere.
  • The consent gate needs no state in JavaScript — group-has-[#tw-17-agree:checked]/gate: hides the hint line, and the controller's single job is keeping the disabled property in step for keyboard and AT users.
  • Decline and Accept share one grid track and one size; only fill differs, which is what equal visual weight means in practice.
  • The terms body is focusable and scroll-isolated, so keyboard users can read it and a flick at its end does not move the page behind.
  • UI change from the CSS original: both actions are 44px, the consent rows gained padding so their targets do not overlap, and the accept green was darkened to clear 4.5:1 with white text.
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 Terms of Service Modal with Letter-Slide and Consent Gate
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-terms-of-service-modal-letter-slide-consent-gate/

Post-signup legal consent with the collection's most restrained motion: a 20px slide down and a fade, ease-out, no overshoot — a document placed on a desk. The gate is the substance: Accept stays disabled until the agreement box is ticked, driven by has-[#tw-17-agree:checked]:, with marketing consent kept separate.
## HTML
```html
<section class="tw-17 w-full min-h-svh grid place-items-center bg-tw17-bg font-tw17-sans px-6 py-10">
  <button type="button" data-tw17-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-[9px] border-0 bg-tw17-ink text-[15px] font-semibold text-white cursor-pointer transition-colors duration-200 hover:bg-tw17-inkhover focus-visible:outline-2 focus-visible:outline-tw17-ink focus-visible:outline-offset-2">Complete signup</button>

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

      <p class="inline-block mb-3.5 px-3 py-1.5 rounded-full bg-tw17-well2 font-tw17-mono text-xs font-bold text-tw17-sub">Terms v3.2 · Updated 2026-07-15</p>
      <h2 id="tw-17-title" class="mb-2 text-[1.3rem] font-extrabold tracking-[-0.015em] leading-tight">Terms of Service &amp; Privacy Policy</h2>
      <p class="mb-[18px] text-sm leading-relaxed text-tw17-sub">Please review before continuing. Your acceptance is required to use Northlight.</p>

      <div tabindex="0" aria-label="Terms of Service, scroll to read in full" class="max-h-[230px] overflow-y-auto overscroll-contain mb-[18px] px-[18px] py-4 rounded-[9px] border border-tw17-line bg-tw17-well focus-visible:outline-2 focus-visible:outline-tw17-ink focus-visible:outline-offset-2">
        <h3 class="mb-1.5 text-[0.85rem] font-bold">1. Acceptance of terms</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">By accessing Northlight ("the Service"), you agree to be bound by these Terms of Service and our Privacy Policy. If you disagree with any part, do not use the Service.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">2. Your account</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. You must notify us immediately of any unauthorized use.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">3. Data collection &amp; use</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">We collect and process personal data as described in our <a href="#" class="font-semibold text-tw17-ink underline">Privacy Policy</a>. This includes account details, usage analytics, and content you create. We do not sell your data to third parties.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">4. Content ownership</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">You retain ownership of content you create in the Service. By using the Service, you grant Northlight a limited license to store, process, and display your content for the purpose of providing the Service.</p>
        <h3 class="mt-3 mb-1.5 text-[0.85rem] font-bold">5. Termination</h3>
        <p class="text-[13px] leading-relaxed text-tw17-sub">You may terminate your account at any time. We may suspend or terminate accounts that violate these Terms. Upon termination, we will delete your data within 30 days per GDPR Article 17.</p>
      </div>

      <div class="group/gate">
        <label class="flex items-start gap-2.5 mb-2.5 py-1 text-[13.5px] leading-relaxed cursor-pointer">
          <input type="checkbox" id="tw-17-agree" required class="shrink-0 mt-0.5 size-4 accent-tw17-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw17-good focus-visible:outline-offset-2">
          <span>I have read and agree to the <a href="#" class="font-semibold text-tw17-ink underline">Terms of Service</a> and <a href="#" class="font-semibold text-tw17-ink underline">Privacy Policy</a></span>
        </label>
        <label class="flex items-start gap-2.5 mb-2.5 py-1 text-[13.5px] leading-relaxed cursor-pointer">
          <input type="checkbox" class="shrink-0 mt-0.5 size-4 accent-tw17-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw17-good focus-visible:outline-offset-2">
          <span>Send me product updates and newsletter (separate, optional consent)</span>
        </label>

        <div class="grid grid-cols-2 gap-2.5 mt-3.5">
          <button type="button" data-tw17-close class="min-h-11 rounded-[9px] border-0 bg-tw17-well2 text-sm font-bold text-tw17-ink cursor-pointer transition-colors duration-200 hover:bg-tw17-well3 focus-visible:outline-2 focus-visible:outline-tw17-ink focus-visible:outline-offset-2">Decline</button>
          <button type="button" data-tw17-accept data-tw17-close disabled class="min-h-11 rounded-[9px] border-0 bg-tw17-good text-sm font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-tw17-goodhover focus-visible:outline-2 focus-visible:outline-tw17-good focus-visible:outline-offset-2 disabled:bg-tw17-well3 disabled:text-tw17-sub disabled:cursor-not-allowed">Accept &amp; continue</button>
        </div>
        <p class="mt-2.5 text-xs text-tw17-sub group-has-[#tw-17-agree:checked]/gate:invisible">Tick the agreement box to enable Accept.</p>
      </div>
    </div>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tw17-bg: #f8fafc;
  --color-tw17-bg: oklch(0.98 0.005 250);
  --color-tw17-card: #ffffff;
  --color-tw17-card: oklch(1 0 0);
  --color-tw17-well: #fafbfc;
  --color-tw17-well: oklch(0.98 0.003 255);
  --color-tw17-well2: #f1f5f9;
  --color-tw17-well2: oklch(0.96 0.006 250);
  --color-tw17-well3: #e2e8f0;
  --color-tw17-well3: oklch(0.92 0.01 250);
  --color-tw17-ink: #0f172a;
  --color-tw17-ink: oklch(0.23 0.04 265);
  --color-tw17-inkhover: #1e293b;
  --color-tw17-inkhover: oklch(0.3 0.03 258);
  --color-tw17-sub: #52627a;
  --color-tw17-sub: oklch(0.48 0.03 255);
  --color-tw17-line: #e2e8f0;
  --color-tw17-line: oklch(0.92 0.01 250);
  --color-tw17-good: #047857;
  --color-tw17-good: oklch(0.52 0.12 164);
  --color-tw17-goodhover: #065f46;
  --color-tw17-goodhover: oklch(0.45 0.1 164);
  --color-tw17-scrim: #0f172a8c;
  --color-tw17-scrim: oklch(0.23 0.04 265 / 0.55);
  --font-tw17-sans: 'Outfit', system-ui, sans-serif;
  --font-tw17-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tw-17 {
  width: 100%;
}

#tw-17-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-17-dlg[open]::backdrop {
    background: oklch(0.23 0.04 265 / 0);
    backdrop-filter: blur(0px);
  }
}

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

## JavaScript
```js
(function () {
  var root = document.querySelector('.tw-17');
  var dlg = root.querySelector('#tw-17-dlg');
  var card = dlg.firstElementChild;
  var agree = root.querySelector('#tw-17-agree');
  var accept = root.querySelector('[data-tw17-accept]');

  agree.addEventListener('change', function () { accept.disabled = !agree.checked; });

  root.querySelector('[data-tw17-open]').addEventListener('click', function () {
    dlg.showModal();
    dlg.querySelector('[tabindex="0"]').focus();
  });
  root.querySelectorAll('[data-tw17-close]').forEach(function (b) {
    // Production: log the accepted version, timestamp, IP and user agent here.
    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

Sobriety with one degree of motion. Demo 05 fades and nothing else; this one adds a short downward slide, which the original's research frames as expressive enough to feel considered and calm enough for a legal surface.

Consent is attested, not assumed. The Accept button is disabled in markup and enabled only when the required box is checked; the has- variant styles it, and the controller mirrors the same condition onto the property.

The version tag is part of the record. "Terms v3.2" is displayed because consent attaches to a version — when the document changes, prior acceptance does not carry over.

Make it yours

  • Match the version format to your convention; uniqueness per revision is the only requirement.
  • Add controller identity, purpose and retention text for GDPR Article 13 jurisdictions.
  • Offer a PDF download — a small share of users want a copy, and it creates a paper trail.
  • For minors, branch into a guardian consent flow; acceptance by a minor is not valid.

Gotchas — read before shipping

  • Unchecked by default, always. A pre-ticked agreement box is invalid consent under GDPR and the CJEU's Planet49 ruling.
  • Legal and marketing consent are separate controls. Bundling them invalidates both.
  • Log version, timestamp, IP and user agent server-side. The UI's job is to collect the act; the record is what survives a dispute.
  • No overshoot here. Elastic motion on a legal dialog measurably lowers trust; if unsure, fall back to demo 05's pure fade.
  • Never time out a terms dialog. Some users read for half an hour, and a timeout would invalidate the consent you just collected.

Browser support

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

:has() is Baseline 2023; where missing, the button stays enabled and the native required checkbox still blocks submission.

Techniques used in this demo

Utilitiesfirst-class utility or variant

HTML & JSno Tailwind involved

Search CodeFronts

Loading…