27 / 36
All 36 in 36 Tailwind CSS Modals
- 01Tailwind Login / Signup Modal with Native Dialog & Starting-Style
- 02Tailwind Password Reset Modal with In-Place Success State
- 03Tailwind Age Verification Gate with :has() and Zero JavaScript
- 04Tailwind Paywall Modal with Masked Article Fade
- 05Tailwind Product Quick-View Modal with has-checked Variant Chips
- 06Tailwind Shopping Cart Slide-Over Drawer with Dialog
- 07Tailwind Timed Newsletter Promo Popup with Frequency Cap
- 08Tailwind Size Guide Overlay with Zero-JS cm/inch Toggle
- 09Tailwind Zero-JS Lightbox Gallery with the target Variant
- 10Tailwind Video Trailer Modal with Aspect-Locked Iframe
- 11Tailwind Destructive Delete Modal with Type-to-Confirm Gate
- 12Tailwind Success / Error Result Modal with Drawn SVG Icon
- 13Tailwind GDPR Cookie Consent Banner with details Disclosure
- 14Tailwind Multi-Step Onboarding Modal with data-step Variants
- 15Tailwind Filter & Sort Drawer that Becomes a Bottom Sheet
- 16Tailwind Contact Support Modal with Live Validation
- 17Tailwind Tabbed Settings Modal with Zero-JS Radio Tabs
- 18Tailwind Command Palette Modal with ⌘K and Roving Selection
- 19Tailwind Confirm Delete Modal with Corner Slide-and-Spin Entrance
- 20Tailwind Login Modal with Staggered Blur-In Content Drop
- 21Tailwind Signup Modal with 3D Origami Fan Deploy Panels
- 22Tailwind Payment Checkout Modal with 3D Paper Fold Entrance
- 23Tailwind HIPAA Patient Consent Modal with Scroll-Gated Agreement
- 24Tailwind Age Verification Modal with Clip-Path Iris Aperture
- 25Tailwind GDPR Cookie Preferences Modal with Unfolding Category Rows
- 26Tailwind Video Lightbox Modal with Chromatic Glitch Materialize
- 27Tailwind Image Gallery Lightbox with Clip-Path Diamond Reveal
- 28Tailwind Add to Cart Confirmation with SVG Liquid Morph Entrance
- 29Tailwind Product Quick View Modal with Elastic Button Morph
- 30Tailwind Newsletter Signup Modal with Depth-Layer Parallax Entrance
- 31Tailwind Exit-Intent Discount Modal with Live Countdown and Coupon Copy
- 32Tailwind Restaurant Reservation Modal with Cascading Fan Panels
- 33Tailwind Order Success Modal with CSS-Only Confetti Burst
- 34Tailwind SaaS Onboarding Welcome Modal with Cascading Blur-In
- 35Tailwind Terms of Service Modal with Letter-Slide and Consent Gate
- 36Tailwind Password Reset Modal with Enumeration-Safe Confirmation
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.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
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">✕</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">◄</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">►</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><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">✕</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">◄</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">►</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;
}
}@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(); });
})();(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.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@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;
}
}@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;
}
}<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">
<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
fixedto 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.
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">✕</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">◄</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">►</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(); });
})();
```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">✕</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">◄</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">►</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>withloading="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
alttext; emptyaltis 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ · 117+ anim | 16.4+ · 17.5+ anim | 128+ · 129+ anim | 111+ · 117+ anim |
clip-path: polygon() animation is Baseline 2018; the dialog's fade sits on the newer gate and degrades to an instant show.