10 / 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 Video Trailer Modal with Aspect-Locked Iframe
A click-to-play trailer overlay: a 16:9 box locked by aspect-video before anything loads, and an iframe whose src is set on open and cleared on close so audio stops the instant the dialog does. Zero layout shift, zero orphaned audio.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tm-10 w-full min-h-svh grid place-items-center bg-tm10-bg font-tm10-sans px-6 py-10">
<button type="button" data-tm10-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm10-ink bg-tm10-btn border border-tm10-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:border-tm10-acc focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2"><span aria-hidden="true">▶</span> Watch trailer</button>
<dialog id="tm-10-dlg" aria-labelledby="tm-10-title" class="m-auto w-[min(780px,94vw)] p-0 border-0 rounded-2xl overflow-hidden text-tm10-ink bg-tm10-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm10-edge)] opacity-0 scale-[0.96] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:scale-100 open:starting:opacity-0 open:starting:scale-[0.96] open:backdrop:bg-tm10-scrim open:backdrop:backdrop-blur-[6px] motion-reduce:transition-none">
<div data-tm10-card>
<div class="flex items-center justify-between gap-3 px-4 py-3 bg-tm10-bar">
<h2 id="tm-10-title" class="text-[15px] font-semibold text-tm10-hi">Official Trailer — “Nightfall”</h2>
<button type="button" data-tm10-close aria-label="Close player" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm10-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:text-white focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2">×</button>
</div>
<div class="relative aspect-video bg-black">
<!-- Ship this: the src is set from data-src on open and cleared on close.
<iframe data-tm10-video title="Official Trailer — Nightfall"
data-src="https://www.youtube-nocookie.com/embed/VIDEO_ID?rel=0"
class="absolute inset-0 size-full border-0"
allow="autoplay; encrypted-media; fullscreen" allowfullscreen></iframe> -->
<div aria-hidden="true" class="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(120%_120%_at_50%_40%,var(--color-tm10-posterA),var(--color-tm10-posterB))]">
<div class="grid place-items-center size-19 pl-1.5 rounded-full text-[26px] bg-tm10-play text-tm10-onacc shadow-[0_0_50px_-6px_var(--color-tm10-acc)] animate-tm10-pulse motion-reduce:animate-none">▶</div>
<span class="font-tm10-mono text-xs tracking-[0.14em] text-tm10-mut">16:9 player · your <iframe> here</span>
</div>
</div>
</div>
</dialog>
</section><section class="tm-10 w-full min-h-svh grid place-items-center bg-tm10-bg font-tm10-sans px-6 py-10">
<button type="button" data-tm10-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm10-ink bg-tm10-btn border border-tm10-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:border-tm10-acc focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2"><span aria-hidden="true">▶</span> Watch trailer</button>
<dialog id="tm-10-dlg" aria-labelledby="tm-10-title" class="m-auto w-[min(780px,94vw)] p-0 border-0 rounded-2xl overflow-hidden text-tm10-ink bg-tm10-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm10-edge)] opacity-0 scale-[0.96] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:scale-100 open:starting:opacity-0 open:starting:scale-[0.96] open:backdrop:bg-tm10-scrim open:backdrop:backdrop-blur-[6px] motion-reduce:transition-none">
<div data-tm10-card>
<div class="flex items-center justify-between gap-3 px-4 py-3 bg-tm10-bar">
<h2 id="tm-10-title" class="text-[15px] font-semibold text-tm10-hi">Official Trailer — “Nightfall”</h2>
<button type="button" data-tm10-close aria-label="Close player" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm10-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:text-white focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2">×</button>
</div>
<div class="relative aspect-video bg-black">
<!-- Ship this: the src is set from data-src on open and cleared on close.
<iframe data-tm10-video title="Official Trailer — Nightfall"
data-src="https://www.youtube-nocookie.com/embed/VIDEO_ID?rel=0"
class="absolute inset-0 size-full border-0"
allow="autoplay; encrypted-media; fullscreen" allowfullscreen></iframe> -->
<div aria-hidden="true" class="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(120%_120%_at_50%_40%,var(--color-tm10-posterA),var(--color-tm10-posterB))]">
<div class="grid place-items-center size-19 pl-1.5 rounded-full text-[26px] bg-tm10-play text-tm10-onacc shadow-[0_0_50px_-6px_var(--color-tm10-acc)] animate-tm10-pulse motion-reduce:animate-none">▶</div>
<span class="font-tm10-mono text-xs tracking-[0.14em] text-tm10-mut">16:9 player · your <iframe> here</span>
</div>
</div>
</div>
</dialog>
</section>@import "tailwindcss";
@theme {
--color-tm10-bg: #080607;
--color-tm10-bg: oklch(0.14 0.012 15);
--color-tm10-card: #0c0809;
--color-tm10-card: oklch(0.16 0.012 15);
--color-tm10-bar: #170f11;
--color-tm10-bar: oklch(0.2 0.018 15);
--color-tm10-btn: #1e1215;
--color-tm10-btn: oklch(0.23 0.04 15);
--color-tm10-btnhover: #2a181c;
--color-tm10-btnhover: oklch(0.28 0.05 15);
--color-tm10-btnedge: #4a2630;
--color-tm10-btnedge: oklch(0.36 0.09 15);
--color-tm10-edge: #2a1e22;
--color-tm10-edge: oklch(0.27 0.02 15);
--color-tm10-posterA: #2c161c;
--color-tm10-posterA: oklch(0.27 0.06 15);
--color-tm10-posterB: #0a0608;
--color-tm10-posterB: oklch(0.15 0.015 15);
--color-tm10-hi: #f7edef;
--color-tm10-hi: oklch(0.95 0.01 15);
--color-tm10-ink: #e9dade;
--color-tm10-ink: oklch(0.9 0.015 15);
--color-tm10-mut: #b3959b;
--color-tm10-mut: oklch(0.72 0.03 15);
--color-tm10-acc: #f2557a;
--color-tm10-acc: oklch(0.68 0.2 15);
--color-tm10-play: #f8768f;
--color-tm10-play: oklch(0.74 0.17 15);
--color-tm10-onacc: #1a0a0d;
--color-tm10-onacc: oklch(0.2 0.04 15);
--color-tm10-scrim: #040303b8;
--color-tm10-scrim: oklch(0.11 0.008 15 / 0.72);
--font-tm10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm10-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tm10-pulse: tm10-pulse 2.4s ease-in-out infinite;
}
.tm-10 {
width: 100%;
}
@keyframes tm10-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
#tm-10-dlg::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-10-dlg[open]::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
}
}@import "tailwindcss";
@theme {
--color-tm10-bg: #080607;
--color-tm10-bg: oklch(0.14 0.012 15);
--color-tm10-card: #0c0809;
--color-tm10-card: oklch(0.16 0.012 15);
--color-tm10-bar: #170f11;
--color-tm10-bar: oklch(0.2 0.018 15);
--color-tm10-btn: #1e1215;
--color-tm10-btn: oklch(0.23 0.04 15);
--color-tm10-btnhover: #2a181c;
--color-tm10-btnhover: oklch(0.28 0.05 15);
--color-tm10-btnedge: #4a2630;
--color-tm10-btnedge: oklch(0.36 0.09 15);
--color-tm10-edge: #2a1e22;
--color-tm10-edge: oklch(0.27 0.02 15);
--color-tm10-posterA: #2c161c;
--color-tm10-posterA: oklch(0.27 0.06 15);
--color-tm10-posterB: #0a0608;
--color-tm10-posterB: oklch(0.15 0.015 15);
--color-tm10-hi: #f7edef;
--color-tm10-hi: oklch(0.95 0.01 15);
--color-tm10-ink: #e9dade;
--color-tm10-ink: oklch(0.9 0.015 15);
--color-tm10-mut: #b3959b;
--color-tm10-mut: oklch(0.72 0.03 15);
--color-tm10-acc: #f2557a;
--color-tm10-acc: oklch(0.68 0.2 15);
--color-tm10-play: #f8768f;
--color-tm10-play: oklch(0.74 0.17 15);
--color-tm10-onacc: #1a0a0d;
--color-tm10-onacc: oklch(0.2 0.04 15);
--color-tm10-scrim: #040303b8;
--color-tm10-scrim: oklch(0.11 0.008 15 / 0.72);
--font-tm10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm10-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tm10-pulse: tm10-pulse 2.4s ease-in-out infinite;
}
.tm-10 {
width: 100%;
}
@keyframes tm10-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
#tm-10-dlg::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-10-dlg[open]::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
}
}(function () {
var root = document.querySelector('.tm-10');
var dlg = root.querySelector('#tm-10-dlg');
var card = root.querySelector('[data-tm10-card]');
var video = root.querySelector('[data-tm10-video]'); // present in production markup
root.querySelector('[data-tm10-open]').addEventListener('click', function () {
if (video && video.dataset.src) video.src = video.dataset.src; // start
dlg.showModal();
});
root.querySelector('[data-tm10-close]').addEventListener('click', function () { dlg.close(); });
dlg.addEventListener('close', function () { if (video) video.src = ''; }); // stop playback
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();
});
})();(function () {
var root = document.querySelector('.tm-10');
var dlg = root.querySelector('#tm-10-dlg');
var card = root.querySelector('[data-tm10-card]');
var video = root.querySelector('[data-tm10-video]'); // present in production markup
root.querySelector('[data-tm10-open]').addEventListener('click', function () {
if (video && video.dataset.src) video.src = video.dataset.src; // start
dlg.showModal();
});
root.querySelector('[data-tm10-close]').addEventListener('click', function () { dlg.close(); });
dlg.addEventListener('close', function () { if (video) video.src = ''; }); // stop playback
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.
<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-tm10-bg: #080607;
--color-tm10-bg: oklch(0.14 0.012 15);
--color-tm10-card: #0c0809;
--color-tm10-card: oklch(0.16 0.012 15);
--color-tm10-bar: #170f11;
--color-tm10-bar: oklch(0.2 0.018 15);
--color-tm10-btn: #1e1215;
--color-tm10-btn: oklch(0.23 0.04 15);
--color-tm10-btnhover: #2a181c;
--color-tm10-btnhover: oklch(0.28 0.05 15);
--color-tm10-btnedge: #4a2630;
--color-tm10-btnedge: oklch(0.36 0.09 15);
--color-tm10-edge: #2a1e22;
--color-tm10-edge: oklch(0.27 0.02 15);
--color-tm10-posterA: #2c161c;
--color-tm10-posterA: oklch(0.27 0.06 15);
--color-tm10-posterB: #0a0608;
--color-tm10-posterB: oklch(0.15 0.015 15);
--color-tm10-hi: #f7edef;
--color-tm10-hi: oklch(0.95 0.01 15);
--color-tm10-ink: #e9dade;
--color-tm10-ink: oklch(0.9 0.015 15);
--color-tm10-mut: #b3959b;
--color-tm10-mut: oklch(0.72 0.03 15);
--color-tm10-acc: #f2557a;
--color-tm10-acc: oklch(0.68 0.2 15);
--color-tm10-play: #f8768f;
--color-tm10-play: oklch(0.74 0.17 15);
--color-tm10-onacc: #1a0a0d;
--color-tm10-onacc: oklch(0.2 0.04 15);
--color-tm10-scrim: #040303b8;
--color-tm10-scrim: oklch(0.11 0.008 15 / 0.72);
--font-tm10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm10-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tm10-pulse: tm10-pulse 2.4s ease-in-out infinite;
}
.tm-10 {
width: 100%;
}
@keyframes tm10-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
#tm-10-dlg::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-10-dlg[open]::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
}
}@import "tailwindcss";
@theme {
--color-tm10-bg: #080607;
--color-tm10-bg: oklch(0.14 0.012 15);
--color-tm10-card: #0c0809;
--color-tm10-card: oklch(0.16 0.012 15);
--color-tm10-bar: #170f11;
--color-tm10-bar: oklch(0.2 0.018 15);
--color-tm10-btn: #1e1215;
--color-tm10-btn: oklch(0.23 0.04 15);
--color-tm10-btnhover: #2a181c;
--color-tm10-btnhover: oklch(0.28 0.05 15);
--color-tm10-btnedge: #4a2630;
--color-tm10-btnedge: oklch(0.36 0.09 15);
--color-tm10-edge: #2a1e22;
--color-tm10-edge: oklch(0.27 0.02 15);
--color-tm10-posterA: #2c161c;
--color-tm10-posterA: oklch(0.27 0.06 15);
--color-tm10-posterB: #0a0608;
--color-tm10-posterB: oklch(0.15 0.015 15);
--color-tm10-hi: #f7edef;
--color-tm10-hi: oklch(0.95 0.01 15);
--color-tm10-ink: #e9dade;
--color-tm10-ink: oklch(0.9 0.015 15);
--color-tm10-mut: #b3959b;
--color-tm10-mut: oklch(0.72 0.03 15);
--color-tm10-acc: #f2557a;
--color-tm10-acc: oklch(0.68 0.2 15);
--color-tm10-play: #f8768f;
--color-tm10-play: oklch(0.74 0.17 15);
--color-tm10-onacc: #1a0a0d;
--color-tm10-onacc: oklch(0.2 0.04 15);
--color-tm10-scrim: #040303b8;
--color-tm10-scrim: oklch(0.11 0.008 15 / 0.72);
--font-tm10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm10-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tm10-pulse: tm10-pulse 2.4s ease-in-out infinite;
}
.tm-10 {
width: 100%;
}
@keyframes tm10-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
#tm-10-dlg::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-10-dlg[open]::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
}
}<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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
aspect-ratio:16/9becomesaspect-video, which is the rare case where the utility is both shorter and more legible than the declaration.- The pulse animation is a
@theme--animate-*entry so the play button readsanimate-tm10-pulse motion-reduce:animate-none— the reduced-motion opt-out sits next to the thing it opts out of. - UI change from the CSS original: the close button went from 34px to 44px, the title bar now uses
justify-betweenwith a real gap so long trailer titles wrap instead of colliding with the ✕, and the poster's label contrast was raised one step.
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 Video Trailer Modal with Aspect-Locked Iframe
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-video-trailer-modal-aspect-locked-iframe/
A click-to-play trailer overlay: a 16:9 box locked by aspect-video before anything loads, and an iframe whose src is set on open and cleared on close so audio stops the instant the dialog does. Zero layout shift, zero orphaned audio.
## HTML
```html
<section class="tm-10 w-full min-h-svh grid place-items-center bg-tm10-bg font-tm10-sans px-6 py-10">
<button type="button" data-tm10-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm10-ink bg-tm10-btn border border-tm10-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:border-tm10-acc focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2"><span aria-hidden="true">▶</span> Watch trailer</button>
<dialog id="tm-10-dlg" aria-labelledby="tm-10-title" class="m-auto w-[min(780px,94vw)] p-0 border-0 rounded-2xl overflow-hidden text-tm10-ink bg-tm10-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm10-edge)] opacity-0 scale-[0.96] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:scale-100 open:starting:opacity-0 open:starting:scale-[0.96] open:backdrop:bg-tm10-scrim open:backdrop:backdrop-blur-[6px] motion-reduce:transition-none">
<div data-tm10-card>
<div class="flex items-center justify-between gap-3 px-4 py-3 bg-tm10-bar">
<h2 id="tm-10-title" class="text-[15px] font-semibold text-tm10-hi">Official Trailer — “Nightfall”</h2>
<button type="button" data-tm10-close aria-label="Close player" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm10-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:text-white focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2">×</button>
</div>
<div class="relative aspect-video bg-black">
<!-- Ship this: the src is set from data-src on open and cleared on close.
<iframe data-tm10-video title="Official Trailer — Nightfall"
data-src="https://www.youtube-nocookie.com/embed/VIDEO_ID?rel=0"
class="absolute inset-0 size-full border-0"
allow="autoplay; encrypted-media; fullscreen" allowfullscreen></iframe> -->
<div aria-hidden="true" class="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(120%_120%_at_50%_40%,var(--color-tm10-posterA),var(--color-tm10-posterB))]">
<div class="grid place-items-center size-19 pl-1.5 rounded-full text-[26px] bg-tm10-play text-tm10-onacc shadow-[0_0_50px_-6px_var(--color-tm10-acc)] animate-tm10-pulse motion-reduce:animate-none">▶</div>
<span class="font-tm10-mono text-xs tracking-[0.14em] text-tm10-mut">16:9 player · your <iframe> here</span>
</div>
</div>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm10-bg: #080607;
--color-tm10-bg: oklch(0.14 0.012 15);
--color-tm10-card: #0c0809;
--color-tm10-card: oklch(0.16 0.012 15);
--color-tm10-bar: #170f11;
--color-tm10-bar: oklch(0.2 0.018 15);
--color-tm10-btn: #1e1215;
--color-tm10-btn: oklch(0.23 0.04 15);
--color-tm10-btnhover: #2a181c;
--color-tm10-btnhover: oklch(0.28 0.05 15);
--color-tm10-btnedge: #4a2630;
--color-tm10-btnedge: oklch(0.36 0.09 15);
--color-tm10-edge: #2a1e22;
--color-tm10-edge: oklch(0.27 0.02 15);
--color-tm10-posterA: #2c161c;
--color-tm10-posterA: oklch(0.27 0.06 15);
--color-tm10-posterB: #0a0608;
--color-tm10-posterB: oklch(0.15 0.015 15);
--color-tm10-hi: #f7edef;
--color-tm10-hi: oklch(0.95 0.01 15);
--color-tm10-ink: #e9dade;
--color-tm10-ink: oklch(0.9 0.015 15);
--color-tm10-mut: #b3959b;
--color-tm10-mut: oklch(0.72 0.03 15);
--color-tm10-acc: #f2557a;
--color-tm10-acc: oklch(0.68 0.2 15);
--color-tm10-play: #f8768f;
--color-tm10-play: oklch(0.74 0.17 15);
--color-tm10-onacc: #1a0a0d;
--color-tm10-onacc: oklch(0.2 0.04 15);
--color-tm10-scrim: #040303b8;
--color-tm10-scrim: oklch(0.11 0.008 15 / 0.72);
--font-tm10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm10-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tm10-pulse: tm10-pulse 2.4s ease-in-out infinite;
}
.tm-10 {
width: 100%;
}
@keyframes tm10-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
#tm-10-dlg::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-10-dlg[open]::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-10');
var dlg = root.querySelector('#tm-10-dlg');
var card = root.querySelector('[data-tm10-card]');
var video = root.querySelector('[data-tm10-video]'); // present in production markup
root.querySelector('[data-tm10-open]').addEventListener('click', function () {
if (video && video.dataset.src) video.src = video.dataset.src; // start
dlg.showModal();
});
root.querySelector('[data-tm10-close]').addEventListener('click', function () { dlg.close(); });
dlg.addEventListener('close', function () { if (video) video.src = ''; }); // stop playback
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();
});
})();
```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 Video Trailer Modal with Aspect-Locked Iframe
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-video-trailer-modal-aspect-locked-iframe/
A click-to-play trailer overlay: a 16:9 box locked by aspect-video before anything loads, and an iframe whose src is set on open and cleared on close so audio stops the instant the dialog does. Zero layout shift, zero orphaned audio.
## HTML
```html
<section class="tm-10 w-full min-h-svh grid place-items-center bg-tm10-bg font-tm10-sans px-6 py-10">
<button type="button" data-tm10-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm10-ink bg-tm10-btn border border-tm10-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:border-tm10-acc focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2"><span aria-hidden="true">▶</span> Watch trailer</button>
<dialog id="tm-10-dlg" aria-labelledby="tm-10-title" class="m-auto w-[min(780px,94vw)] p-0 border-0 rounded-2xl overflow-hidden text-tm10-ink bg-tm10-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm10-edge)] opacity-0 scale-[0.96] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:scale-100 open:starting:opacity-0 open:starting:scale-[0.96] open:backdrop:bg-tm10-scrim open:backdrop:backdrop-blur-[6px] motion-reduce:transition-none">
<div data-tm10-card>
<div class="flex items-center justify-between gap-3 px-4 py-3 bg-tm10-bar">
<h2 id="tm-10-title" class="text-[15px] font-semibold text-tm10-hi">Official Trailer — “Nightfall”</h2>
<button type="button" data-tm10-close aria-label="Close player" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm10-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm10-btnhover hover:text-white focus-visible:outline-2 focus-visible:outline-tm10-acc focus-visible:outline-offset-2">×</button>
</div>
<div class="relative aspect-video bg-black">
<!-- Ship this: the src is set from data-src on open and cleared on close.
<iframe data-tm10-video title="Official Trailer — Nightfall"
data-src="https://www.youtube-nocookie.com/embed/VIDEO_ID?rel=0"
class="absolute inset-0 size-full border-0"
allow="autoplay; encrypted-media; fullscreen" allowfullscreen></iframe> -->
<div aria-hidden="true" class="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-[radial-gradient(120%_120%_at_50%_40%,var(--color-tm10-posterA),var(--color-tm10-posterB))]">
<div class="grid place-items-center size-19 pl-1.5 rounded-full text-[26px] bg-tm10-play text-tm10-onacc shadow-[0_0_50px_-6px_var(--color-tm10-acc)] animate-tm10-pulse motion-reduce:animate-none">▶</div>
<span class="font-tm10-mono text-xs tracking-[0.14em] text-tm10-mut">16:9 player · your <iframe> here</span>
</div>
</div>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm10-bg: #080607;
--color-tm10-bg: oklch(0.14 0.012 15);
--color-tm10-card: #0c0809;
--color-tm10-card: oklch(0.16 0.012 15);
--color-tm10-bar: #170f11;
--color-tm10-bar: oklch(0.2 0.018 15);
--color-tm10-btn: #1e1215;
--color-tm10-btn: oklch(0.23 0.04 15);
--color-tm10-btnhover: #2a181c;
--color-tm10-btnhover: oklch(0.28 0.05 15);
--color-tm10-btnedge: #4a2630;
--color-tm10-btnedge: oklch(0.36 0.09 15);
--color-tm10-edge: #2a1e22;
--color-tm10-edge: oklch(0.27 0.02 15);
--color-tm10-posterA: #2c161c;
--color-tm10-posterA: oklch(0.27 0.06 15);
--color-tm10-posterB: #0a0608;
--color-tm10-posterB: oklch(0.15 0.015 15);
--color-tm10-hi: #f7edef;
--color-tm10-hi: oklch(0.95 0.01 15);
--color-tm10-ink: #e9dade;
--color-tm10-ink: oklch(0.9 0.015 15);
--color-tm10-mut: #b3959b;
--color-tm10-mut: oklch(0.72 0.03 15);
--color-tm10-acc: #f2557a;
--color-tm10-acc: oklch(0.68 0.2 15);
--color-tm10-play: #f8768f;
--color-tm10-play: oklch(0.74 0.17 15);
--color-tm10-onacc: #1a0a0d;
--color-tm10-onacc: oklch(0.2 0.04 15);
--color-tm10-scrim: #040303b8;
--color-tm10-scrim: oklch(0.11 0.008 15 / 0.72);
--font-tm10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm10-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tm10-pulse: tm10-pulse 2.4s ease-in-out infinite;
}
.tm-10 {
width: 100%;
}
@keyframes tm10-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.06);
}
}
#tm-10-dlg::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-10-dlg[open]::backdrop {
background: oklch(0.11 0.008 15 / 0);
backdrop-filter: blur(0px);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-10');
var dlg = root.querySelector('#tm-10-dlg');
var card = root.querySelector('[data-tm10-card]');
var video = root.querySelector('[data-tm10-video]'); // present in production markup
root.querySelector('[data-tm10-open]').addEventListener('click', function () {
if (video && video.dataset.src) video.src = video.dataset.src; // start
dlg.showModal();
});
root.querySelector('[data-tm10-close]').addEventListener('click', function () { dlg.close(); });
dlg.addEventListener('close', function () { if (video) video.src = ''; }); // stop playback
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 src swap is the whole trick. An iframe that is merely hidden keeps playing: the audio continues, the network keeps streaming. Setting src = '' on the dialog's close event tears the player down, and restoring it from data-src on open rebuilds it. Two lines, and the only correct answer short of the vendor player API.
The box is locked first. aspect-video reserves the 16:9 space before the embed loads, so nothing below it moves — the difference between a good CLS score and a bad one on any page with a trailer.
The poster is a placeholder. The commented iframe in the markup is the production element; the pulsing play button stands in for a real thumbnail so the demo has nothing to fetch.
Make it yours
- Point
data-srcat any embeddable URL; append?autoplay=1to start on open. - Swap the poster for a real thumbnail
<img>with the play button layered over it. - Go full-bleed for a hero trailer by dropping
w-[min(780px,94vw)]tow-screen h-screenand removing the rounding.
Gotchas — read before shipping
- Clear the iframe
srcon close — or call the player API's pause. A hidden iframe is a playing iframe. - Lock the aspect box before load. Letting the embed define its own height is the most common source of a video-page CLS failure.
- Give the iframe a descriptive
titleand the rightallow/allowfullscreenattributes; withouttitlea screen reader announces only "iframe". aspect-videoon a flex child can be overridden by the parent's stretch behaviour — keep the frame a plain block inside the card, as here.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ · 117+ anim | 16.4+ · 17.5+ anim | 128+ · 129+ anim | 111+ · 117+ anim |
<dialog> and aspect-ratio are Baseline; only the enter/exit animation depends on @starting-style.