36 Tailwind CSS Modals
23 / 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 HIPAA Patient Consent Modal with Scroll-Gated Agreement

A patient authorization sheet whose entrance is deliberately nothing: a 200ms opacity fade, no transform, because decorative motion measurably lowers perceived security on medical consent. The port's real work is the gate — has-[#tw-05-agree:checked]: plus a scroll flag turns the consent button on only when the patient has both read and agreed.

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-05 w-full min-h-svh grid place-items-center bg-tw05-bg font-tw05-sans px-6 py-10">
  <button type="button" data-tw05-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-[9px] border-0 text-[15px] font-semibold text-white bg-tw05-brand shadow-[0_8px_20px_-6px_var(--color-tw05-glow)] cursor-pointer transition-colors duration-200 hover:bg-tw05-brandhover focus-visible:outline-2 focus-visible:outline-tw05-brand focus-visible:outline-offset-2">Start telehealth visit</button>

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

      <p class="inline-flex items-center gap-2 mb-4 px-3 py-1.5 rounded-full bg-tw05-tint text-xs font-semibold tracking-wide text-tw05-brand"><span aria-hidden="true">&#9877;</span>HIPAA-compliant consent · Form v2.3</p>
      <h2 id="tw-05-title" class="mb-2 text-[1.3rem] font-extrabold tracking-[-0.015em] leading-tight">Authorization to share medical information</h2>
      <p class="mb-[18px] text-sm leading-relaxed text-tw05-sub text-pretty">Please review before your telehealth visit. This authorization is required to connect you with a licensed provider.</p>

      <div tabindex="0" data-tw05-body aria-label="Consent text, scroll to read in full" class="max-h-[220px] overflow-y-auto overscroll-contain mb-[18px] px-[18px] py-4 rounded-[9px] border border-tw05-line bg-tw05-well focus-visible:outline-2 focus-visible:outline-tw05-brand focus-visible:outline-offset-2">
        <h3 class="mb-1.5 text-[0.85rem] font-bold text-tw05-ink">What we collect and share</h3>
        <p class="text-[13px] leading-relaxed text-tw05-sub">To provide telehealth services, we collect: your date of birth, insurance ID, symptom description, current medications, and appointment history. We share this with your assigned provider and, when medically necessary, with your primary care physician.</p>
        <h3 class="mt-3.5 mb-1.5 text-[0.85rem] font-bold text-tw05-ink">Your rights under HIPAA</h3>
        <p class="text-[13px] leading-relaxed text-tw05-sub">You have the right to (1) access your PHI, (2) request amendments, (3) receive an accounting of disclosures, and (4) file a complaint. Read the full <a href="#" class="text-tw05-brand underline">Notice of Privacy Practices</a> and <a href="#" class="text-tw05-brand underline">Rights Under HIPAA</a>.</p>
        <h3 class="mt-3.5 mb-1.5 text-[0.85rem] font-bold text-tw05-ink">Withdrawing consent</h3>
        <p class="text-[13px] leading-relaxed text-tw05-sub">You may withdraw this authorization at any time by contacting our Privacy Officer at <a href="#" class="text-tw05-brand underline">privacy@example-health.com</a>. Withdrawal is effective going forward and does not affect PHI already disclosed.</p>
      </div>

      <div class="group/gate" data-tw05-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-05-agree" required class="shrink-0 mt-0.5 size-4 accent-tw05-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw05-good focus-visible:outline-offset-2">
          <span>I have read and understand this authorization. I consent to the collection and sharing of my PHI as described above for this telehealth visit.</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-tw05-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw05-good focus-visible:outline-offset-2">
          <span>Optional: send me appointment reminders and health tips (separate consent)</span>
        </label>

        <div class="grid grid-cols-2 gap-2.5 mt-3.5">
          <button type="button" data-tw05-close class="min-h-11 rounded-[9px] border-0 text-sm font-bold text-tw05-ink bg-tw05-well2 cursor-pointer transition-colors duration-200 hover:bg-tw05-well3 focus-visible:outline-2 focus-visible:outline-tw05-ink focus-visible:outline-offset-2">I don't consent</button>
          <button type="button" data-tw05-close disabled class="min-h-11 rounded-[9px] border-0 text-sm font-bold text-white bg-tw05-good cursor-pointer transition-colors duration-200 hover:bg-tw05-goodhover focus-visible:outline-2 focus-visible:outline-tw05-good focus-visible:outline-offset-2 disabled:bg-tw05-well3 disabled:text-tw05-sub disabled:cursor-not-allowed group-has-[#tw-05-agree:checked]/gate:group-data-[read=true]/gate:enabled:bg-tw05-good">Sign and begin visit</button>
        </div>
        <p class="mt-2.5 text-xs text-tw05-sub group-data-[read=true]/gate:has-[#tw-05-agree:checked]:invisible">Scroll the authorization to the end and tick the box to continue.</p>
      </div>
    </div>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-tw05-bg: #f0f7fc;
  --color-tw05-bg: oklch(0.97 0.015 235);
  --color-tw05-card: #ffffff;
  --color-tw05-card: oklch(1 0 0);
  --color-tw05-well: #fafbfc;
  --color-tw05-well: oklch(0.98 0.003 255);
  --color-tw05-well2: #f1f5f9;
  --color-tw05-well2: oklch(0.96 0.006 250);
  --color-tw05-well3: #e2e8f0;
  --color-tw05-well3: oklch(0.92 0.01 250);
  --color-tw05-ink: #0f172a;
  --color-tw05-ink: oklch(0.23 0.04 265);
  --color-tw05-sub: #52627a;
  --color-tw05-sub: oklch(0.48 0.03 255);
  --color-tw05-line: #dbe3ec;
  --color-tw05-line: oklch(0.9 0.012 245);
  --color-tw05-brand: #2563eb;
  --color-tw05-brand: oklch(0.53 0.2 262);
  --color-tw05-brandhover: #1d4ed8;
  --color-tw05-brandhover: oklch(0.48 0.2 262);
  --color-tw05-tint: #3b82f61a;
  --color-tw05-tint: oklch(0.62 0.19 259 / 0.1);
  --color-tw05-glow: #3b82f680;
  --color-tw05-glow: oklch(0.62 0.19 259 / 0.5);
  --color-tw05-good: #047857;
  --color-tw05-good: oklch(0.52 0.12 164);
  --color-tw05-goodhover: #065f46;
  --color-tw05-goodhover: oklch(0.45 0.1 164);
  --color-tw05-scrim: #0f172a8c;
  --color-tw05-scrim: oklch(0.23 0.04 265 / 0.55);
  --font-tw05-sans: 'Outfit', system-ui, sans-serif;
}

.tw-05 {
  width: 100%;
}

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

@media (prefers-reduced-motion: reduce) {
  #tw-05-dlg::backdrop {
    transition-duration: .001s;
  }
}
(function () {
  var root = document.querySelector('.tw-05');
  var dlg = root.querySelector('#tw-05-dlg');
  var card = dlg.firstElementChild;
  var body = root.querySelector('[data-tw05-body]');
  var gate = root.querySelector('[data-tw05-gate]');
  var agree = root.querySelector('#tw-05-agree');
  var submit = gate.querySelector('button[disabled], button:last-of-type');

  function markRead() {
    if (body.scrollTop + body.clientHeight >= body.scrollHeight - 4) gate.dataset.read = 'true';
    sync();
  }
  function sync() {
    submit.disabled = !(gate.dataset.read === 'true' && agree.checked);
  }
  body.addEventListener('scroll', markRead);
  agree.addEventListener('change', sync);

  root.querySelector('[data-tw05-open]').addEventListener('click', function () {
    dlg.showModal();
    body.focus();
    // Short consent text may already be fully visible — do not trap the patient.
    markRead();
  });
  root.querySelectorAll('[data-tw05-close]').forEach(function (b) {
    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-tw05-bg: #f0f7fc;
  --color-tw05-bg: oklch(0.97 0.015 235);
  --color-tw05-card: #ffffff;
  --color-tw05-card: oklch(1 0 0);
  --color-tw05-well: #fafbfc;
  --color-tw05-well: oklch(0.98 0.003 255);
  --color-tw05-well2: #f1f5f9;
  --color-tw05-well2: oklch(0.96 0.006 250);
  --color-tw05-well3: #e2e8f0;
  --color-tw05-well3: oklch(0.92 0.01 250);
  --color-tw05-ink: #0f172a;
  --color-tw05-ink: oklch(0.23 0.04 265);
  --color-tw05-sub: #52627a;
  --color-tw05-sub: oklch(0.48 0.03 255);
  --color-tw05-line: #dbe3ec;
  --color-tw05-line: oklch(0.9 0.012 245);
  --color-tw05-brand: #2563eb;
  --color-tw05-brand: oklch(0.53 0.2 262);
  --color-tw05-brandhover: #1d4ed8;
  --color-tw05-brandhover: oklch(0.48 0.2 262);
  --color-tw05-tint: #3b82f61a;
  --color-tw05-tint: oklch(0.62 0.19 259 / 0.1);
  --color-tw05-glow: #3b82f680;
  --color-tw05-glow: oklch(0.62 0.19 259 / 0.5);
  --color-tw05-good: #047857;
  --color-tw05-good: oklch(0.52 0.12 164);
  --color-tw05-goodhover: #065f46;
  --color-tw05-goodhover: oklch(0.45 0.1 164);
  --color-tw05-scrim: #0f172a8c;
  --color-tw05-scrim: oklch(0.23 0.04 265 / 0.55);
  --font-tw05-sans: 'Outfit', system-ui, sans-serif;
}

.tw-05 {
  width: 100%;
}

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

@media (prefers-reduced-motion: reduce) {
  #tw-05-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 only entrance utilities on this dialog are an opacity pair. That is the port: resisting the collection's own house style where the content demands it.
  • The gate's two facts live in the DOM — data-read on the wrapper and :checked on the input — so group-data-[read=true]/gate: and has-[…:checked] can express the enabled state directly, and the controller only mirrors it onto the disabled property for keyboard and AT users.
  • disabled: utilities carry the visual state; never rely on colour alone — the hint line under the buttons says what is missing and disappears once both conditions are met.
  • The consent body is tabindex="0" so it is reachable and scrollable by keyboard, and it announces itself with an aria-label.
  • UI change from the CSS original: both actions are 44px, the consent rows have padding so their tap targets do not collide, and the primary action is a darker green that clears 4.5:1 against 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 HIPAA Patient Consent Modal with Scroll-Gated Agreement
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-hipaa-consent-modal-scroll-gated-agreement/

A patient authorization sheet whose entrance is deliberately nothing: a 200ms opacity fade, no transform, because decorative motion measurably lowers perceived security on medical consent. The port's real work is the gate — has-[#tw-05-agree:checked]: plus a scroll flag turns the consent button on only when the patient has both read and agreed.
## HTML
```html
<section class="tw-05 w-full min-h-svh grid place-items-center bg-tw05-bg font-tw05-sans px-6 py-10">
  <button type="button" data-tw05-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-[9px] border-0 text-[15px] font-semibold text-white bg-tw05-brand shadow-[0_8px_20px_-6px_var(--color-tw05-glow)] cursor-pointer transition-colors duration-200 hover:bg-tw05-brandhover focus-visible:outline-2 focus-visible:outline-tw05-brand focus-visible:outline-offset-2">Start telehealth visit</button>

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

      <p class="inline-flex items-center gap-2 mb-4 px-3 py-1.5 rounded-full bg-tw05-tint text-xs font-semibold tracking-wide text-tw05-brand"><span aria-hidden="true">&#9877;</span>HIPAA-compliant consent · Form v2.3</p>
      <h2 id="tw-05-title" class="mb-2 text-[1.3rem] font-extrabold tracking-[-0.015em] leading-tight">Authorization to share medical information</h2>
      <p class="mb-[18px] text-sm leading-relaxed text-tw05-sub text-pretty">Please review before your telehealth visit. This authorization is required to connect you with a licensed provider.</p>

      <div tabindex="0" data-tw05-body aria-label="Consent text, scroll to read in full" class="max-h-[220px] overflow-y-auto overscroll-contain mb-[18px] px-[18px] py-4 rounded-[9px] border border-tw05-line bg-tw05-well focus-visible:outline-2 focus-visible:outline-tw05-brand focus-visible:outline-offset-2">
        <h3 class="mb-1.5 text-[0.85rem] font-bold text-tw05-ink">What we collect and share</h3>
        <p class="text-[13px] leading-relaxed text-tw05-sub">To provide telehealth services, we collect: your date of birth, insurance ID, symptom description, current medications, and appointment history. We share this with your assigned provider and, when medically necessary, with your primary care physician.</p>
        <h3 class="mt-3.5 mb-1.5 text-[0.85rem] font-bold text-tw05-ink">Your rights under HIPAA</h3>
        <p class="text-[13px] leading-relaxed text-tw05-sub">You have the right to (1) access your PHI, (2) request amendments, (3) receive an accounting of disclosures, and (4) file a complaint. Read the full <a href="#" class="text-tw05-brand underline">Notice of Privacy Practices</a> and <a href="#" class="text-tw05-brand underline">Rights Under HIPAA</a>.</p>
        <h3 class="mt-3.5 mb-1.5 text-[0.85rem] font-bold text-tw05-ink">Withdrawing consent</h3>
        <p class="text-[13px] leading-relaxed text-tw05-sub">You may withdraw this authorization at any time by contacting our Privacy Officer at <a href="#" class="text-tw05-brand underline">privacy@example-health.com</a>. Withdrawal is effective going forward and does not affect PHI already disclosed.</p>
      </div>

      <div class="group/gate" data-tw05-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-05-agree" required class="shrink-0 mt-0.5 size-4 accent-tw05-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw05-good focus-visible:outline-offset-2">
          <span>I have read and understand this authorization. I consent to the collection and sharing of my PHI as described above for this telehealth visit.</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-tw05-good cursor-pointer focus-visible:outline-2 focus-visible:outline-tw05-good focus-visible:outline-offset-2">
          <span>Optional: send me appointment reminders and health tips (separate consent)</span>
        </label>

        <div class="grid grid-cols-2 gap-2.5 mt-3.5">
          <button type="button" data-tw05-close class="min-h-11 rounded-[9px] border-0 text-sm font-bold text-tw05-ink bg-tw05-well2 cursor-pointer transition-colors duration-200 hover:bg-tw05-well3 focus-visible:outline-2 focus-visible:outline-tw05-ink focus-visible:outline-offset-2">I don't consent</button>
          <button type="button" data-tw05-close disabled class="min-h-11 rounded-[9px] border-0 text-sm font-bold text-white bg-tw05-good cursor-pointer transition-colors duration-200 hover:bg-tw05-goodhover focus-visible:outline-2 focus-visible:outline-tw05-good focus-visible:outline-offset-2 disabled:bg-tw05-well3 disabled:text-tw05-sub disabled:cursor-not-allowed group-has-[#tw-05-agree:checked]/gate:group-data-[read=true]/gate:enabled:bg-tw05-good">Sign and begin visit</button>
        </div>
        <p class="mt-2.5 text-xs text-tw05-sub group-data-[read=true]/gate:has-[#tw-05-agree:checked]:invisible">Scroll the authorization to the end and tick the box to continue.</p>
      </div>
    </div>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tw05-bg: #f0f7fc;
  --color-tw05-bg: oklch(0.97 0.015 235);
  --color-tw05-card: #ffffff;
  --color-tw05-card: oklch(1 0 0);
  --color-tw05-well: #fafbfc;
  --color-tw05-well: oklch(0.98 0.003 255);
  --color-tw05-well2: #f1f5f9;
  --color-tw05-well2: oklch(0.96 0.006 250);
  --color-tw05-well3: #e2e8f0;
  --color-tw05-well3: oklch(0.92 0.01 250);
  --color-tw05-ink: #0f172a;
  --color-tw05-ink: oklch(0.23 0.04 265);
  --color-tw05-sub: #52627a;
  --color-tw05-sub: oklch(0.48 0.03 255);
  --color-tw05-line: #dbe3ec;
  --color-tw05-line: oklch(0.9 0.012 245);
  --color-tw05-brand: #2563eb;
  --color-tw05-brand: oklch(0.53 0.2 262);
  --color-tw05-brandhover: #1d4ed8;
  --color-tw05-brandhover: oklch(0.48 0.2 262);
  --color-tw05-tint: #3b82f61a;
  --color-tw05-tint: oklch(0.62 0.19 259 / 0.1);
  --color-tw05-glow: #3b82f680;
  --color-tw05-glow: oklch(0.62 0.19 259 / 0.5);
  --color-tw05-good: #047857;
  --color-tw05-good: oklch(0.52 0.12 164);
  --color-tw05-goodhover: #065f46;
  --color-tw05-goodhover: oklch(0.45 0.1 164);
  --color-tw05-scrim: #0f172a8c;
  --color-tw05-scrim: oklch(0.23 0.04 265 / 0.55);
  --font-tw05-sans: 'Outfit', system-ui, sans-serif;
}

.tw-05 {
  width: 100%;
}

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

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

## JavaScript
```js
(function () {
  var root = document.querySelector('.tw-05');
  var dlg = root.querySelector('#tw-05-dlg');
  var card = dlg.firstElementChild;
  var body = root.querySelector('[data-tw05-body]');
  var gate = root.querySelector('[data-tw05-gate]');
  var agree = root.querySelector('#tw-05-agree');
  var submit = gate.querySelector('button[disabled], button:last-of-type');

  function markRead() {
    if (body.scrollTop + body.clientHeight >= body.scrollHeight - 4) gate.dataset.read = 'true';
    sync();
  }
  function sync() {
    submit.disabled = !(gate.dataset.read === 'true' && agree.checked);
  }
  body.addEventListener('scroll', markRead);
  agree.addEventListener('change', sync);

  root.querySelector('[data-tw05-open]').addEventListener('click', function () {
    dlg.showModal();
    body.focus();
    // Short consent text may already be fully visible — do not trap the patient.
    markRead();
  });
  root.querySelectorAll('[data-tw05-close]').forEach(function (b) {
    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

The entrance is the point. Every other demo in this collection earns a signature; this one earns restraint. A 200ms fade with zero transform is what institutional software looks like, and the original's research note is explicit that a paper fold here would cost trust.

The gate is CSS plus one boolean. The consent body sets data-read="true" when scrolled to the bottom; the checkbox is read by has-. The button's enabled state is the intersection, so it cannot drift from the two facts it depends on.

Two checkboxes, never one. PHI authorization and marketing are separate controls, both unchecked — bundling them is not a compact layout, it is an invalid authorization under 45 CFR § 164.508.

Make it yours

  • Replace the copy with your Privacy Officer's approved text; the layout is the deliverable here, not the wording.
  • Keep the version tag in the footer and log it server-side with timestamp, IP and user agent — the audit trail is the point of the tag.
  • For pediatric consent add a guardian block below the agreement row; the gate logic is unchanged.
  • Telehealth variants should carry an emergency disclaimer directly above the primary button, where it cannot be scrolled past.

Gotchas — read before shipping

  • Never pre-check the authorization box. A pre-checked consent is an audit finding, not a conversion optimisation.
  • Do not animate this dialog. The sober fade is the researched choice; adding a fold or a spring here actively reduces perceived security.
  • Keep the modal light. Medical consent in a dark surface reads as less trustworthy even when the rest of the product is dark.
  • A scroll gate must have a keyboard path: the body is focusable and scrollable with arrow keys, and reaching the end via keyboard sets the same flag as a mouse wheel.
  • overscroll-contain matters on iOS — without it a flick inside the consent text scrolls the page underneath and the patient loses their place mid-form.

Browser support

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

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

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…