36 Tailwind CSS Modals
29 / 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 Product Quick View Modal with Elastic Button Morph

A browse-mode product preview that expands from a button-sized pill into a full card — scale, translate and border-radius animating together on an elastic curve. The port states all three on the dialog as open:starting: values, so the morph's start shape lives beside its end shape.

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

  <dialog id="tw-11-dlg" aria-labelledby="tw-11-title" class="m-auto w-[min(720px,94vw)] p-0 border-0 overflow-hidden text-left text-tw11-ink bg-tw11-card shadow-[0_40px_60px_-20px_#1e293b59] will-change-transform opacity-0 scale-[0.08] translate-y-[200px] rounded-full transition-[opacity,scale,translate,border-radius,overlay,display] duration-[650ms] ease-[cubic-bezier(.68,-0.55,.32,1.55)] transition-discrete open:opacity-100 open:scale-100 open:translate-y-0 open:rounded-2xl open:starting:opacity-0 open:starting:scale-[0.08] open:starting:translate-y-[200px] open:starting:rounded-full open:backdrop:bg-tw11-scrim open:backdrop:backdrop-blur-[4px] motion-reduce:transition-[opacity] motion-reduce:duration-200 motion-reduce:scale-100 motion-reduce:translate-y-0 motion-reduce:rounded-2xl">
    <button type="button" autofocus data-tw11-close aria-label="Close quick view" class="absolute top-3.5 right-3.5 z-10 grid place-items-center size-11 rounded-lg border-0 bg-tw11-glass backdrop-blur-[4px] text-[15px] text-tw11-sub cursor-pointer transition-colors duration-200 hover:bg-white hover:text-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">&#10005;</button>

    <div class="grid grid-cols-[1.1fr_1fr] max-[560px]:grid-cols-1">
      <div data-tw11-gallery class="p-6">
        <div data-tw11-hero class="grid place-items-center aspect-square rounded-xl text-[88px]" aria-hidden="true">&#128092;</div>
        <div role="radiogroup" aria-label="Colour" class="flex justify-center gap-2 mt-3.5">
          <button type="button" role="radio" aria-checked="true" aria-label="Slate" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swslate ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
          <button type="button" role="radio" aria-checked="false" aria-label="Coral" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swcoral ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
          <button type="button" role="radio" aria-checked="false" aria-label="Olive" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swolive ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
          <button type="button" role="radio" aria-checked="false" aria-label="Mustard" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swmustard ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
        </div>
      </div>

      <div class="flex flex-col px-[26px] pt-7 pb-6">
        <p class="mb-1.5 text-xs font-bold uppercase tracking-[0.08em] text-tw11-sub">Terra Studio</p>
        <h2 id="tw-11-title" class="mb-2.5 font-tw11-display text-2xl font-extrabold tracking-[-0.015em] leading-tight">Everyday Crossbody</h2>
        <p class="flex items-baseline gap-2.5 mb-3">
          <span class="text-[1.35rem] font-extrabold text-tw11-brand tabular-nums">$128</span>
          <span class="text-sm text-tw11-sub line-through tabular-nums">$168</span>
        </p>
        <p class="mb-4 text-sm leading-relaxed text-tw11-sub text-pretty">Vegetable-tanned leather · Recycled brass hardware · Fits a 13-inch laptop.</p>

        <div role="radiogroup" aria-label="Size" class="flex gap-2 mb-4">
          <button type="button" role="radio" aria-checked="false" class="min-w-11 min-h-11 px-3.5 rounded-lg border border-tw11-line bg-white text-sm font-bold text-tw11-ink cursor-pointer transition-colors duration-200 hover:border-tw11-ink aria-[checked=true]:bg-tw11-ink aria-[checked=true]:text-tw11-card aria-[checked=true]:border-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">S</button>
          <button type="button" role="radio" aria-checked="true" class="min-w-11 min-h-11 px-3.5 rounded-lg border border-tw11-line bg-white text-sm font-bold text-tw11-ink cursor-pointer transition-colors duration-200 hover:border-tw11-ink aria-[checked=true]:bg-tw11-ink aria-[checked=true]:text-tw11-card aria-[checked=true]:border-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">M</button>
          <button type="button" role="radio" aria-checked="false" class="min-w-11 min-h-11 px-3.5 rounded-lg border border-tw11-line bg-white text-sm font-bold text-tw11-ink cursor-pointer transition-colors duration-200 hover:border-tw11-ink aria-[checked=true]:bg-tw11-ink aria-[checked=true]:text-tw11-card aria-[checked=true]:border-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">L</button>
        </div>

        <button type="button" data-tw11-close class="mt-auto min-h-11 px-5 rounded-[9px] border-0 bg-tw11-brand text-[15px] font-bold text-white tabular-nums shadow-[0_8px_18px_-6px_var(--color-tw11-glow)] cursor-pointer transition-[background-color,translate] duration-200 hover:bg-tw11-branddark hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tw11-brand focus-visible:outline-offset-[3px] motion-reduce:hover:translate-y-0">Add to cart · $128</button>
        <a href="#" class="mt-2.5 text-center text-[13px] text-tw11-sub no-underline hover:text-tw11-ink hover:underline">View full details &rarr;</a>
      </div>
    </div>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-tw11-bg: #fdf7f4;
  --color-tw11-bg: oklch(0.97 0.012 40);
  --color-tw11-card: #fafaf7;
  --color-tw11-card: oklch(0.98 0.004 95);
  --color-tw11-ink: #1e293b;
  --color-tw11-ink: oklch(0.3 0.03 258);
  --color-tw11-sub: #55657c;
  --color-tw11-sub: oklch(0.5 0.03 255);
  --color-tw11-line: #e5e0d5;
  --color-tw11-line: oklch(0.9 0.015 85);
  --color-tw11-brand: #e05252;
  --color-tw11-brand: oklch(0.62 0.17 22);
  --color-tw11-branddark: #c23e3e;
  --color-tw11-branddark: oklch(0.55 0.17 22);
  --color-tw11-glow: #ff6b6b80;
  --color-tw11-glow: oklch(0.7 0.17 22 / 0.5);
  --color-tw11-glass: #ffffffb3;
  --color-tw11-glass: oklch(1 0 0 / 0.7);
  --color-tw11-swslate: #1e293b;
  --color-tw11-swslate: oklch(0.3 0.03 258);
  --color-tw11-swcoral: #ff6b6b;
  --color-tw11-swcoral: oklch(0.7 0.17 22);
  --color-tw11-swolive: #84cc16;
  --color-tw11-swolive: oklch(0.77 0.19 128);
  --color-tw11-swmustard: #eab308;
  --color-tw11-swmustard: oklch(0.79 0.16 86);
  --color-tw11-scrim: #1e293b80;
  --color-tw11-scrim: oklch(0.3 0.03 258 / 0.5);
  --font-tw11-sans: 'Outfit', system-ui, sans-serif;
  --font-tw11-display: 'Playfair Display', Georgia, serif;
}

.tw-11 {
  width: 100%;
}
/* Placeholder gallery surfaces — swap for real product photography. */

.tw-11 [data-tw11-gallery] {
  background-image: linear-gradient(180deg, oklch(0.95 0.03 85), oklch(0.92 0.04 80));
}

.tw-11 [data-tw11-hero] {
  background-image: linear-gradient(135deg, oklch(0.93 0.04 350), oklch(0.88 0.06 280));
}

#tw-11-dlg::backdrop {
  background: oklch(0.3 0.03 258 / 0);
  backdrop-filter: blur(0px);
  transition: background .3s ease, backdrop-filter .3s ease, overlay .3s allow-discrete, display .3s allow-discrete;
}

@starting-style {
  #tw-11-dlg[open]::backdrop {
    background: oklch(0.3 0.03 258 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tw-11-dlg::backdrop {
    transition-duration: .001s;
  }
}
(function () {
  var root = document.querySelector('.tw-11');
  var dlg = root.querySelector('#tw-11-dlg');
  root.querySelector('[data-tw11-open]').addEventListener('click', function () { dlg.showModal(); });
  root.querySelectorAll('[data-tw11-close]').forEach(function (b) {
    b.addEventListener('click', function () { dlg.close(); });
  });
  // Radio groups: one checked per group, state written to ARIA so styling follows semantics.
  root.querySelectorAll('[role=radiogroup]').forEach(function (group) {
    group.addEventListener('click', function (e) {
      var btn = e.target.closest('[role=radio]');
      if (!btn) return;
      group.querySelectorAll('[role=radio]').forEach(function (r) { r.setAttribute('aria-checked', String(r === btn)); });
    });
  });
  dlg.addEventListener('click', function (e) {
    var r = dlg.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-tw11-bg: #fdf7f4;
  --color-tw11-bg: oklch(0.97 0.012 40);
  --color-tw11-card: #fafaf7;
  --color-tw11-card: oklch(0.98 0.004 95);
  --color-tw11-ink: #1e293b;
  --color-tw11-ink: oklch(0.3 0.03 258);
  --color-tw11-sub: #55657c;
  --color-tw11-sub: oklch(0.5 0.03 255);
  --color-tw11-line: #e5e0d5;
  --color-tw11-line: oklch(0.9 0.015 85);
  --color-tw11-brand: #e05252;
  --color-tw11-brand: oklch(0.62 0.17 22);
  --color-tw11-branddark: #c23e3e;
  --color-tw11-branddark: oklch(0.55 0.17 22);
  --color-tw11-glow: #ff6b6b80;
  --color-tw11-glow: oklch(0.7 0.17 22 / 0.5);
  --color-tw11-glass: #ffffffb3;
  --color-tw11-glass: oklch(1 0 0 / 0.7);
  --color-tw11-swslate: #1e293b;
  --color-tw11-swslate: oklch(0.3 0.03 258);
  --color-tw11-swcoral: #ff6b6b;
  --color-tw11-swcoral: oklch(0.7 0.17 22);
  --color-tw11-swolive: #84cc16;
  --color-tw11-swolive: oklch(0.77 0.19 128);
  --color-tw11-swmustard: #eab308;
  --color-tw11-swmustard: oklch(0.79 0.16 86);
  --color-tw11-scrim: #1e293b80;
  --color-tw11-scrim: oklch(0.3 0.03 258 / 0.5);
  --font-tw11-sans: 'Outfit', system-ui, sans-serif;
  --font-tw11-display: 'Playfair Display', Georgia, serif;
}

.tw-11 {
  width: 100%;
}
/* Placeholder gallery surfaces — swap for real product photography. */

.tw-11 [data-tw11-gallery] {
  background-image: linear-gradient(180deg, oklch(0.95 0.03 85), oklch(0.92 0.04 80));
}

.tw-11 [data-tw11-hero] {
  background-image: linear-gradient(135deg, oklch(0.93 0.04 350), oklch(0.88 0.06 280));
}

#tw-11-dlg::backdrop {
  background: oklch(0.3 0.03 258 / 0);
  backdrop-filter: blur(0px);
  transition: background .3s ease, backdrop-filter .3s ease, overlay .3s allow-discrete, display .3s allow-discrete;
}

@starting-style {
  #tw-11-dlg[open]::backdrop {
    background: oklch(0.3 0.03 258 / 0);
    backdrop-filter: blur(0px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #tw-11-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=Playfair+Display:wght@700;800&display=swap">

What changed in the port

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

  • Four properties, one curve, one class list: the morph's start shape (scale-[0.08] translate-y-[200px] rounded-full) is stated as open:starting: right beside the card's resting shape.
  • Because the dialog itself is the card here, the light-dismiss test uses the dialog's own rect — a click inside the modal box is always inside the card.
  • Selection styling hangs off aria-[checked=true], so the controller sets one attribute and both the visual state and the announced state move together.
  • will-change-transform is kept from the original for a real reason: at 8% scale the compositor needs the hint or the first frames render unpromoted.
  • UI change from the CSS original: swatches sit inside 44px buttons (the coloured dot is a child, so it can stay 24px), sizes are 44px chips, and the close control grew to 44px.
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 Product Quick View Modal with Elastic Button Morph
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-quick-view-modal-morphing-button-elastic/

A browse-mode product preview that expands from a button-sized pill into a full card — scale, translate and border-radius animating together on an elastic curve. The port states all three on the dialog as open:starting: values, so the morph's start shape lives beside its end shape.
## HTML
```html
<section class="tw-11 w-full min-h-svh grid place-items-center bg-tw11-bg font-tw11-sans px-6 py-10">
  <button type="button" data-tw11-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw11-ink bg-transparent text-[15px] font-semibold text-tw11-ink cursor-pointer transition-colors duration-200 hover:bg-tw11-ink hover:text-tw11-card focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">Quick view</button>

  <dialog id="tw-11-dlg" aria-labelledby="tw-11-title" class="m-auto w-[min(720px,94vw)] p-0 border-0 overflow-hidden text-left text-tw11-ink bg-tw11-card shadow-[0_40px_60px_-20px_#1e293b59] will-change-transform opacity-0 scale-[0.08] translate-y-[200px] rounded-full transition-[opacity,scale,translate,border-radius,overlay,display] duration-[650ms] ease-[cubic-bezier(.68,-0.55,.32,1.55)] transition-discrete open:opacity-100 open:scale-100 open:translate-y-0 open:rounded-2xl open:starting:opacity-0 open:starting:scale-[0.08] open:starting:translate-y-[200px] open:starting:rounded-full open:backdrop:bg-tw11-scrim open:backdrop:backdrop-blur-[4px] motion-reduce:transition-[opacity] motion-reduce:duration-200 motion-reduce:scale-100 motion-reduce:translate-y-0 motion-reduce:rounded-2xl">
    <button type="button" autofocus data-tw11-close aria-label="Close quick view" class="absolute top-3.5 right-3.5 z-10 grid place-items-center size-11 rounded-lg border-0 bg-tw11-glass backdrop-blur-[4px] text-[15px] text-tw11-sub cursor-pointer transition-colors duration-200 hover:bg-white hover:text-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">&#10005;</button>

    <div class="grid grid-cols-[1.1fr_1fr] max-[560px]:grid-cols-1">
      <div data-tw11-gallery class="p-6">
        <div data-tw11-hero class="grid place-items-center aspect-square rounded-xl text-[88px]" aria-hidden="true">&#128092;</div>
        <div role="radiogroup" aria-label="Colour" class="flex justify-center gap-2 mt-3.5">
          <button type="button" role="radio" aria-checked="true" aria-label="Slate" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swslate ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
          <button type="button" role="radio" aria-checked="false" aria-label="Coral" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swcoral ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
          <button type="button" role="radio" aria-checked="false" aria-label="Olive" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swolive ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
          <button type="button" role="radio" aria-checked="false" aria-label="Mustard" class="grid place-items-center size-11 rounded-full cursor-pointer bg-transparent border-0 focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2"><span class="block size-6 rounded-full bg-tw11-swmustard ring-1 ring-tw11-line transition-[scale,box-shadow] duration-200 [button[aria-checked=true]_&amp;]:scale-110 [button[aria-checked=true]_&amp;]:ring-2 [button[aria-checked=true]_&amp;]:ring-tw11-ink"></span></button>
        </div>
      </div>

      <div class="flex flex-col px-[26px] pt-7 pb-6">
        <p class="mb-1.5 text-xs font-bold uppercase tracking-[0.08em] text-tw11-sub">Terra Studio</p>
        <h2 id="tw-11-title" class="mb-2.5 font-tw11-display text-2xl font-extrabold tracking-[-0.015em] leading-tight">Everyday Crossbody</h2>
        <p class="flex items-baseline gap-2.5 mb-3">
          <span class="text-[1.35rem] font-extrabold text-tw11-brand tabular-nums">$128</span>
          <span class="text-sm text-tw11-sub line-through tabular-nums">$168</span>
        </p>
        <p class="mb-4 text-sm leading-relaxed text-tw11-sub text-pretty">Vegetable-tanned leather · Recycled brass hardware · Fits a 13-inch laptop.</p>

        <div role="radiogroup" aria-label="Size" class="flex gap-2 mb-4">
          <button type="button" role="radio" aria-checked="false" class="min-w-11 min-h-11 px-3.5 rounded-lg border border-tw11-line bg-white text-sm font-bold text-tw11-ink cursor-pointer transition-colors duration-200 hover:border-tw11-ink aria-[checked=true]:bg-tw11-ink aria-[checked=true]:text-tw11-card aria-[checked=true]:border-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">S</button>
          <button type="button" role="radio" aria-checked="true" class="min-w-11 min-h-11 px-3.5 rounded-lg border border-tw11-line bg-white text-sm font-bold text-tw11-ink cursor-pointer transition-colors duration-200 hover:border-tw11-ink aria-[checked=true]:bg-tw11-ink aria-[checked=true]:text-tw11-card aria-[checked=true]:border-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">M</button>
          <button type="button" role="radio" aria-checked="false" class="min-w-11 min-h-11 px-3.5 rounded-lg border border-tw11-line bg-white text-sm font-bold text-tw11-ink cursor-pointer transition-colors duration-200 hover:border-tw11-ink aria-[checked=true]:bg-tw11-ink aria-[checked=true]:text-tw11-card aria-[checked=true]:border-tw11-ink focus-visible:outline-2 focus-visible:outline-tw11-ink focus-visible:outline-offset-2">L</button>
        </div>

        <button type="button" data-tw11-close class="mt-auto min-h-11 px-5 rounded-[9px] border-0 bg-tw11-brand text-[15px] font-bold text-white tabular-nums shadow-[0_8px_18px_-6px_var(--color-tw11-glow)] cursor-pointer transition-[background-color,translate] duration-200 hover:bg-tw11-branddark hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tw11-brand focus-visible:outline-offset-[3px] motion-reduce:hover:translate-y-0">Add to cart · $128</button>
        <a href="#" class="mt-2.5 text-center text-[13px] text-tw11-sub no-underline hover:text-tw11-ink hover:underline">View full details &rarr;</a>
      </div>
    </div>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tw11-bg: #fdf7f4;
  --color-tw11-bg: oklch(0.97 0.012 40);
  --color-tw11-card: #fafaf7;
  --color-tw11-card: oklch(0.98 0.004 95);
  --color-tw11-ink: #1e293b;
  --color-tw11-ink: oklch(0.3 0.03 258);
  --color-tw11-sub: #55657c;
  --color-tw11-sub: oklch(0.5 0.03 255);
  --color-tw11-line: #e5e0d5;
  --color-tw11-line: oklch(0.9 0.015 85);
  --color-tw11-brand: #e05252;
  --color-tw11-brand: oklch(0.62 0.17 22);
  --color-tw11-branddark: #c23e3e;
  --color-tw11-branddark: oklch(0.55 0.17 22);
  --color-tw11-glow: #ff6b6b80;
  --color-tw11-glow: oklch(0.7 0.17 22 / 0.5);
  --color-tw11-glass: #ffffffb3;
  --color-tw11-glass: oklch(1 0 0 / 0.7);
  --color-tw11-swslate: #1e293b;
  --color-tw11-swslate: oklch(0.3 0.03 258);
  --color-tw11-swcoral: #ff6b6b;
  --color-tw11-swcoral: oklch(0.7 0.17 22);
  --color-tw11-swolive: #84cc16;
  --color-tw11-swolive: oklch(0.77 0.19 128);
  --color-tw11-swmustard: #eab308;
  --color-tw11-swmustard: oklch(0.79 0.16 86);
  --color-tw11-scrim: #1e293b80;
  --color-tw11-scrim: oklch(0.3 0.03 258 / 0.5);
  --font-tw11-sans: 'Outfit', system-ui, sans-serif;
  --font-tw11-display: 'Playfair Display', Georgia, serif;
}

.tw-11 {
  width: 100%;
}
/* Placeholder gallery surfaces — swap for real product photography. */

.tw-11 [data-tw11-gallery] {
  background-image: linear-gradient(180deg, oklch(0.95 0.03 85), oklch(0.92 0.04 80));
}

.tw-11 [data-tw11-hero] {
  background-image: linear-gradient(135deg, oklch(0.93 0.04 350), oklch(0.88 0.06 280));
}

#tw-11-dlg::backdrop {
  background: oklch(0.3 0.03 258 / 0);
  backdrop-filter: blur(0px);
  transition: background .3s ease, backdrop-filter .3s ease, overlay .3s allow-discrete, display .3s allow-discrete;
}

@starting-style {
  #tw-11-dlg[open]::backdrop {
    background: oklch(0.3 0.03 258 / 0);
    backdrop-filter: blur(0px);
  }
}

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

## JavaScript
```js
(function () {
  var root = document.querySelector('.tw-11');
  var dlg = root.querySelector('#tw-11-dlg');
  root.querySelector('[data-tw11-open]').addEventListener('click', function () { dlg.showModal(); });
  root.querySelectorAll('[data-tw11-close]').forEach(function (b) {
    b.addEventListener('click', function () { dlg.close(); });
  });
  // Radio groups: one checked per group, state written to ARIA so styling follows semantics.
  root.querySelectorAll('[role=radiogroup]').forEach(function (group) {
    group.addEventListener('click', function (e) {
      var btn = e.target.closest('[role=radio]');
      if (!btn) return;
      group.querySelectorAll('[role=radio]').forEach(function (r) { r.setAttribute('aria-checked', String(r === btn)); });
    });
  });
  dlg.addEventListener('click', function (e) {
    var r = dlg.getBoundingClientRect();
    if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
  });
})();
```

How this works

The morph is four properties on one curve. Scale, translate, radius and opacity all transition together; the elastic cubic-bezier(.68,-.55,.32,1.55) undershoots then overshoots, which is what sells "button becomes card".

Selection state is ARIA-first. The colour swatches and sizes are buttons in a role="radiogroup"; their styling reads aria-[checked=true]:, so the visual and the announced state cannot disagree.

Focus starts on close. Quick view is exploratory: the most common next action is dismissing to keep browsing, and Add to cart is two Tabs away.

Make it yours

  • Calm the motion for premium brands: swap the elastic curve for cubic-bezier(.34,1.36,.44,1), or drop to a fade and scale.
  • Move the morph origin with origin-bottom-right when the trigger is a floating button rather than a grid card.
  • Wire swatches to swap the hero image; for patterned goods swap the src rather than a gradient.
  • Add a wishlist toggle beside Add to cart — the actions row is already a grid.

Gotchas — read before shipping

  • An 0.08 start scale renders text about a pixel tall for a frame. Keep will-change: transform (here on the card) so the compositor promotes the layer and interpolates smoothly.
  • Elastic undershoot is precisely the motion vestibular-sensitive users react to — the motion-reduce: path must be a plain fade, not a slower bounce.
  • Quick view is for browse-mode categories. High-consideration purchases need the full product page, and a preview that replaces it costs conversions rather than saving clicks.
  • Add to cart from a quick view must lead somewhere — a silent close leaves the shopper unsure anything happened.
  • Swatch and size chips need real 44px targets; decorative 24px dots are unusable on touch and invisible to coarse pointers.

Browser support

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

transform and border-radius transitions are universal; the @starting-style morph is the newer gate and degrades to an instant show.

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…