36 Tailwind CSS Modals
27 / 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 Image Gallery Lightbox with Clip-Path Diamond Reveal

A portfolio lightbox that opens through a diamond aperture — four polygon vertices expanding from the centre past the frame's corners. The port states both polygons as arbitrary properties on the stage, adds real arrow-key navigation the original only described, and keeps the counter and caption legible on near-black.

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

  <dialog id="tw-09-dlg" aria-labelledby="tw-09-title" class="group w-full max-w-none h-full max-h-none m-0 p-8 border-0 bg-transparent open:grid open:place-items-center open:backdrop:bg-tw09-scrim open:backdrop:backdrop-blur-[10px]">
    <button type="button" data-tw09-close aria-label="Close gallery" class="fixed top-5 right-6 grid place-items-center size-11 rounded-full border border-tw09-edge bg-tw09-glass text-base text-tw09-ink cursor-pointer transition-[background-color,rotate] duration-200 hover:bg-tw09-glass2 hover:rotate-90 focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2 motion-reduce:hover:rotate-0">&#10005;</button>
    <button type="button" data-tw09-prev aria-label="Previous image" class="fixed left-2 sm:left-6 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-full border border-tw09-edge bg-tw09-glass text-base text-tw09-ink cursor-pointer transition-colors duration-200 hover:bg-tw09-glass2 focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2">&#9668;</button>
    <button type="button" autofocus data-tw09-next aria-label="Next image" class="fixed right-2 sm:right-6 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-full border border-tw09-edge bg-tw09-glass text-base text-tw09-ink cursor-pointer transition-colors duration-200 hover:bg-tw09-glass2 focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2">&#9658;</button>

    <div class="w-[min(820px,100%,calc((100svh-200px)*1.6))] opacity-0 [clip-path:polygon(50%_50%,50%_50%,50%_50%,50%_50%)] transition-[opacity,clip-path] duration-[600ms] ease-[cubic-bezier(.65,0,.35,1)] group-open:opacity-100 group-open:[clip-path:polygon(50%_-50%,150%_50%,50%_150%,-50%_50%)] motion-reduce:transition-opacity motion-reduce:[clip-path:none]">
      <h2 id="tw-09-title" class="mb-3.5 text-center text-xs font-bold uppercase tracking-[0.14em] text-tw09-sub">Portfolio · 2026 selections</h2>

      <div data-tw09-frame class="relative aspect-[16/10] rounded-md overflow-hidden bg-tw09-frame">
        <span data-tw09-counter class="absolute top-3.5 right-3.5 px-3 py-1.5 rounded-full bg-tw09-chip backdrop-blur-[4px] font-tw09-mono text-xs font-bold tracking-wide text-tw09-ink" aria-live="polite">3 / 12</span>
      </div>

      <div class="mt-4 text-center">
        <p class="text-sm leading-relaxed text-tw09-sub"><strong data-tw09-caption class="block mb-0.5 font-semibold text-tw09-ink">Northlight fintech dashboard · Q3 2026</strong>Design system refresh · 200+ components migrated · 4× ship velocity</p>
      </div>
    </div>
  </dialog>
</section>
@import "tailwindcss";

@theme {
  --color-tw09-bg: #0a0a0f;
  --color-tw09-bg: oklch(0.14 0.01 285);
  --color-tw09-frame: #1b1b2e;
  --color-tw09-frame: oklch(0.24 0.03 285);
  --color-tw09-ink: #f5f5fa;
  --color-tw09-ink: oklch(0.97 0.005 285);
  --color-tw09-sub: #a9b3c6;
  --color-tw09-sub: oklch(0.77 0.02 260);
  --color-tw09-edge: #f5f5fa33;
  --color-tw09-edge: oklch(0.97 0.005 285 / 0.2);
  --color-tw09-glass: #ffffff0f;
  --color-tw09-glass: oklch(1 0 0 / 0.06);
  --color-tw09-glass2: #ffffff24;
  --color-tw09-glass2: oklch(1 0 0 / 0.14);
  --color-tw09-chip: #00000080;
  --color-tw09-chip: oklch(0 0 0 / 0.5);
  --color-tw09-brand: #a78bfa;
  --color-tw09-brand: oklch(0.73 0.16 296);
  --color-tw09-scrim: #000000f0;
  --color-tw09-scrim: oklch(0 0 0 / 0.94);
  --font-tw09-sans: 'Outfit', system-ui, sans-serif;
  --font-tw09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tw-09 {
  width: 100%;
}
/* Placeholder artwork for the frame — swap for a real <img srcset>. */

#tw-09-dlg [data-tw09-frame] {
  background-image: linear-gradient(135deg, oklch(0.62 0.19 285), oklch(0.65 0.22 350), oklch(0.72 0.18 55));
}

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

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

@media (prefers-reduced-motion: reduce) {
  #tw-09-dlg::backdrop {
    transition-duration: .001s;
  }
}
(function () {
  var root = document.querySelector('.tw-09');
  var dlg = root.querySelector('#tw-09-dlg');
  var counter = root.querySelector('[data-tw09-counter]');
  var caption = root.querySelector('[data-tw09-caption]');
  var SHOTS = [
    'Atlas travel booking · Q1 2026',
    'Harbor payments console · Q2 2026',
    'Northlight fintech dashboard · Q3 2026',
    'Fieldnote research tool · Q3 2026'
  ];
  var i = 2;

  function paint() {
    counter.textContent = (i + 1) + ' / ' + SHOTS.length;
    caption.textContent = SHOTS[i];
  }
  function step(n) { i = (i + n + SHOTS.length) % SHOTS.length; paint(); }

  root.querySelector('[data-tw09-open]').addEventListener('click', function () { dlg.showModal(); paint(); });
  root.querySelector('[data-tw09-close]').addEventListener('click', function () { dlg.close(); });
  root.querySelector('[data-tw09-prev]').addEventListener('click', function () { step(-1); });
  root.querySelector('[data-tw09-next]').addEventListener('click', function () { step(1); });

  dlg.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowLeft') { e.preventDefault(); step(-1); }
    else if (e.key === 'ArrowRight') { e.preventDefault(); step(1); }
  });
  // Full-bleed dialog: the edge controls are fixed and sit outside the stage's rect,
  // so light-dismiss tests the event target instead of coordinates.
  dlg.addEventListener('click', function (e) { if (e.target === dlg) 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-tw09-bg: #0a0a0f;
  --color-tw09-bg: oklch(0.14 0.01 285);
  --color-tw09-frame: #1b1b2e;
  --color-tw09-frame: oklch(0.24 0.03 285);
  --color-tw09-ink: #f5f5fa;
  --color-tw09-ink: oklch(0.97 0.005 285);
  --color-tw09-sub: #a9b3c6;
  --color-tw09-sub: oklch(0.77 0.02 260);
  --color-tw09-edge: #f5f5fa33;
  --color-tw09-edge: oklch(0.97 0.005 285 / 0.2);
  --color-tw09-glass: #ffffff0f;
  --color-tw09-glass: oklch(1 0 0 / 0.06);
  --color-tw09-glass2: #ffffff24;
  --color-tw09-glass2: oklch(1 0 0 / 0.14);
  --color-tw09-chip: #00000080;
  --color-tw09-chip: oklch(0 0 0 / 0.5);
  --color-tw09-brand: #a78bfa;
  --color-tw09-brand: oklch(0.73 0.16 296);
  --color-tw09-scrim: #000000f0;
  --color-tw09-scrim: oklch(0 0 0 / 0.94);
  --font-tw09-sans: 'Outfit', system-ui, sans-serif;
  --font-tw09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tw-09 {
  width: 100%;
}
/* Placeholder artwork for the frame — swap for a real <img srcset>. */

#tw-09-dlg [data-tw09-frame] {
  background-image: linear-gradient(135deg, oklch(0.62 0.19 285), oklch(0.65 0.22 350), oklch(0.72 0.18 55));
}

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

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

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

What changed in the port

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

  • Two polygons and one transition list replace the original's open-only keyframe; the aperture now closes as well as opens because both states are utilities on the same element.
  • Arrow-key navigation is wired here rather than left as a note — the counter is aria-live="polite", so a screen-reader user hears their position change.
  • The placeholder artwork is one CSS line on a data attribute, kept out of the utility list precisely because it is the part you delete when real images arrive.
  • The edge controls are fixed to the viewport rather than absolutely placed inside the stage, so the diamond reveal never clips them.
  • UI change from the CSS original: all three controls are 44px, the caption's lead line is a real <strong> block, and the muted text was lifted to clear 4.5:1 on the near-black backdrop.
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 Image Gallery Lightbox with Clip-Path Diamond Reveal
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-image-gallery-lightbox-clip-path-diamond-reveal/

A portfolio lightbox that opens through a diamond aperture — four polygon vertices expanding from the centre past the frame's corners. The port states both polygons as arbitrary properties on the stage, adds real arrow-key navigation the original only described, and keeps the counter and caption legible on near-black.
## HTML
```html
<section class="tw-09 w-full min-h-svh grid place-items-center bg-tw09-bg font-tw09-sans px-6 py-10">
  <button type="button" data-tw09-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw09-brand bg-transparent text-[15px] font-semibold text-tw09-brand cursor-pointer transition-colors duration-200 hover:bg-tw09-brand hover:text-tw09-bg focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2">Open gallery · 12 photos</button>

  <dialog id="tw-09-dlg" aria-labelledby="tw-09-title" class="group w-full max-w-none h-full max-h-none m-0 p-8 border-0 bg-transparent open:grid open:place-items-center open:backdrop:bg-tw09-scrim open:backdrop:backdrop-blur-[10px]">
    <button type="button" data-tw09-close aria-label="Close gallery" class="fixed top-5 right-6 grid place-items-center size-11 rounded-full border border-tw09-edge bg-tw09-glass text-base text-tw09-ink cursor-pointer transition-[background-color,rotate] duration-200 hover:bg-tw09-glass2 hover:rotate-90 focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2 motion-reduce:hover:rotate-0">&#10005;</button>
    <button type="button" data-tw09-prev aria-label="Previous image" class="fixed left-2 sm:left-6 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-full border border-tw09-edge bg-tw09-glass text-base text-tw09-ink cursor-pointer transition-colors duration-200 hover:bg-tw09-glass2 focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2">&#9668;</button>
    <button type="button" autofocus data-tw09-next aria-label="Next image" class="fixed right-2 sm:right-6 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-full border border-tw09-edge bg-tw09-glass text-base text-tw09-ink cursor-pointer transition-colors duration-200 hover:bg-tw09-glass2 focus-visible:outline-2 focus-visible:outline-tw09-brand focus-visible:outline-offset-2">&#9658;</button>

    <div class="w-[min(820px,100%,calc((100svh-200px)*1.6))] opacity-0 [clip-path:polygon(50%_50%,50%_50%,50%_50%,50%_50%)] transition-[opacity,clip-path] duration-[600ms] ease-[cubic-bezier(.65,0,.35,1)] group-open:opacity-100 group-open:[clip-path:polygon(50%_-50%,150%_50%,50%_150%,-50%_50%)] motion-reduce:transition-opacity motion-reduce:[clip-path:none]">
      <h2 id="tw-09-title" class="mb-3.5 text-center text-xs font-bold uppercase tracking-[0.14em] text-tw09-sub">Portfolio · 2026 selections</h2>

      <div data-tw09-frame class="relative aspect-[16/10] rounded-md overflow-hidden bg-tw09-frame">
        <span data-tw09-counter class="absolute top-3.5 right-3.5 px-3 py-1.5 rounded-full bg-tw09-chip backdrop-blur-[4px] font-tw09-mono text-xs font-bold tracking-wide text-tw09-ink" aria-live="polite">3 / 12</span>
      </div>

      <div class="mt-4 text-center">
        <p class="text-sm leading-relaxed text-tw09-sub"><strong data-tw09-caption class="block mb-0.5 font-semibold text-tw09-ink">Northlight fintech dashboard · Q3 2026</strong>Design system refresh · 200+ components migrated · 4× ship velocity</p>
      </div>
    </div>
  </dialog>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tw09-bg: #0a0a0f;
  --color-tw09-bg: oklch(0.14 0.01 285);
  --color-tw09-frame: #1b1b2e;
  --color-tw09-frame: oklch(0.24 0.03 285);
  --color-tw09-ink: #f5f5fa;
  --color-tw09-ink: oklch(0.97 0.005 285);
  --color-tw09-sub: #a9b3c6;
  --color-tw09-sub: oklch(0.77 0.02 260);
  --color-tw09-edge: #f5f5fa33;
  --color-tw09-edge: oklch(0.97 0.005 285 / 0.2);
  --color-tw09-glass: #ffffff0f;
  --color-tw09-glass: oklch(1 0 0 / 0.06);
  --color-tw09-glass2: #ffffff24;
  --color-tw09-glass2: oklch(1 0 0 / 0.14);
  --color-tw09-chip: #00000080;
  --color-tw09-chip: oklch(0 0 0 / 0.5);
  --color-tw09-brand: #a78bfa;
  --color-tw09-brand: oklch(0.73 0.16 296);
  --color-tw09-scrim: #000000f0;
  --color-tw09-scrim: oklch(0 0 0 / 0.94);
  --font-tw09-sans: 'Outfit', system-ui, sans-serif;
  --font-tw09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tw-09 {
  width: 100%;
}
/* Placeholder artwork for the frame — swap for a real <img srcset>. */

#tw-09-dlg [data-tw09-frame] {
  background-image: linear-gradient(135deg, oklch(0.62 0.19 285), oklch(0.65 0.22 350), oklch(0.72 0.18 55));
}

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

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

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

## JavaScript
```js
(function () {
  var root = document.querySelector('.tw-09');
  var dlg = root.querySelector('#tw-09-dlg');
  var counter = root.querySelector('[data-tw09-counter]');
  var caption = root.querySelector('[data-tw09-caption]');
  var SHOTS = [
    'Atlas travel booking · Q1 2026',
    'Harbor payments console · Q2 2026',
    'Northlight fintech dashboard · Q3 2026',
    'Fieldnote research tool · Q3 2026'
  ];
  var i = 2;

  function paint() {
    counter.textContent = (i + 1) + ' / ' + SHOTS.length;
    caption.textContent = SHOTS[i];
  }
  function step(n) { i = (i + n + SHOTS.length) % SHOTS.length; paint(); }

  root.querySelector('[data-tw09-open]').addEventListener('click', function () { dlg.showModal(); paint(); });
  root.querySelector('[data-tw09-close]').addEventListener('click', function () { dlg.close(); });
  root.querySelector('[data-tw09-prev]').addEventListener('click', function () { step(-1); });
  root.querySelector('[data-tw09-next]').addEventListener('click', function () { step(1); });

  dlg.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowLeft') { e.preventDefault(); step(-1); }
    else if (e.key === 'ArrowRight') { e.preventDefault(); step(1); }
  });
  // Full-bleed dialog: the edge controls are fixed and sit outside the stage's rect,
  // so light-dismiss tests the event target instead of coordinates.
  dlg.addEventListener('click', function (e) { if (e.target === dlg) dlg.close(); });
})();
```

How this works

The diamond is two polygons. Collapsed, all four vertices sit at the centre; open, they pass the corners. Because the states are utilities on the stage, the reveal reverses on close without a second rule.

Keyboard navigation is implemented, not implied. ← and → move through the set and update the counter and caption; the browser's own Esc closes.

Focus lands on Next. In a gallery the likely next action is advancing, not closing — and the close control stays one Tab away.

Make it yours

  • Change the aperture shape by editing the open polygon — a hexagon or a rectangle uses the same expand-from-centre idea.
  • Add EXIF beneath the caption for photography sets; read it server-side and ship it as JSON.
  • Swap the placeholder frame for <img srcset> with loading="lazy" beyond the first couple of slides.
  • For commerce, put a View product action under the caption and thumbnails along the bottom edge.

Gotchas — read before shipping

  • Every gallery image needs descriptive alt text; empty alt is correct only for decoration, which a portfolio image never is.
  • Ship srcset. A 4K JPEG delivered to a phone is the most common performance failure in lightboxes.
  • Virtualise long sets — rendering two hundred images on open costs the page its interactivity.
  • The clip-path animation is compositor-only, so it is cheap; the temptation to also animate width or height is what makes lightboxes jank.
  • Arrow keys must not scroll the page behind the dialog — preventDefault() in the handler, as the controller does.

Browser support

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

clip-path: polygon() animation is Baseline 2018; the dialog's fade sits on the newer gate and degrades to an instant show.

Techniques used in this demo

Arbitraryneeds bracket syntax

HTML & JSno Tailwind involved

Search CodeFronts

Loading…