36 / 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 Password Reset Modal with Enumeration-Safe Confirmation
The collection's closing bookend: demo 01's corner swing reused on a warmer palette, proving the entrance library travels. The port adds what the original only warned about — a neutral, enumeration-safe confirmation that swaps in place via one data-view attribute and announces itself to screen readers.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tw-18 w-full min-h-svh grid place-items-center bg-tw18-bg font-tw18-sans px-6 py-10">
<button type="button" data-tw18-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw18-line bg-tw18-card text-[15px] font-semibold text-tw18-brand cursor-pointer transition-[background-color,border-color] duration-200 hover:bg-tw18-well hover:border-tw18-brand focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">Forgot password?</button>
<dialog id="tw-18-dlg" data-view="form" aria-labelledby="tw-18-title" class="group m-auto w-[min(400px,94vw)] p-0 border-0 rounded-[14px] text-center text-tw18-ink bg-tw18-card shadow-[0_30px_60px_-20px_#0f172a47] origin-bottom-right opacity-0 transition-[opacity,translate,rotate,scale,overlay,display] duration-[400ms] ease-[cubic-bezier(.34,1.36,.44,1)] transition-discrete open:opacity-100 open:translate-x-0 open:translate-y-0 open:rotate-0 open:scale-100 open:starting:opacity-0 open:starting:translate-x-6 open:starting:translate-y-8 open:starting:rotate-3 open:starting:scale-[0.96] open:backdrop:bg-tw18-scrim open:backdrop:backdrop-blur-[4px] motion-reduce:transition-[opacity] motion-reduce:duration-150">
<div class="relative px-[30px] pt-8 pb-6">
<button type="button" data-tw18-close aria-label="Close dialog" class="absolute top-3.5 right-3.5 grid place-items-center size-9 rounded-lg border-0 bg-transparent text-[15px] text-tw18-sub cursor-pointer transition-colors duration-200 hover:bg-tw18-well hover:text-tw18-ink focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">✕</button>
<div class="group-data-[view=sent]:hidden">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-tint text-2xl text-tw18-brand">🔑</span>
<h2 id="tw-18-title" class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Reset your password</h2>
<p class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">Enter your email and we'll send a link to reset your password. Link expires in 30 minutes.</p>
<form data-tw18-form class="flex flex-col gap-3">
<label class="flex flex-col gap-1.5 text-left">
<span class="text-xs font-bold uppercase tracking-[0.06em] text-tw18-sub">Email address</span>
<input type="email" autofocus required autocomplete="email" placeholder="you@work.com" class="min-h-11 px-3.5 rounded-lg border border-tw18-line bg-white text-[15px] text-tw18-ink placeholder:text-tw18-sub transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tw18-brand focus:shadow-[0_0_0_3px_var(--color-tw18-ring)] user-invalid:border-tw18-bad">
</label>
<button type="submit" class="min-h-11 mt-1 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white shadow-[0_8px_18px_-6px_var(--color-tw18-glow)] cursor-pointer transition-[background-color,translate] duration-200 hover:bg-tw18-branddark hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px] motion-reduce:hover:translate-y-0">Send reset link →</button>
</form>
</div>
<div hidden class="group-data-[view=sent]:block">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-goodtint text-2xl text-tw18-good">✉</span>
<h3 class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Check your inbox</h3>
<p aria-live="polite" class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">If an account exists for that address, a reset link is on its way. It expires in 30 minutes.</p>
<button type="button" data-tw18-ack data-tw18-close class="w-full min-h-11 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-tw18-branddark focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px]">Got it</button>
</div>
<div class="pt-4 mt-4 border-t border-tw18-line">
<button type="button" data-tw18-close class="min-h-11 px-3 bg-transparent border-0 text-sm font-semibold text-tw18-brand cursor-pointer hover:underline focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">← Return to sign in</button>
<p class="text-xs text-tw18-sub">Can't access this email? <a href="#" class="font-semibold text-tw18-brand underline">Contact support</a></p>
</div>
</div>
</dialog>
</section><section class="tw-18 w-full min-h-svh grid place-items-center bg-tw18-bg font-tw18-sans px-6 py-10">
<button type="button" data-tw18-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw18-line bg-tw18-card text-[15px] font-semibold text-tw18-brand cursor-pointer transition-[background-color,border-color] duration-200 hover:bg-tw18-well hover:border-tw18-brand focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">Forgot password?</button>
<dialog id="tw-18-dlg" data-view="form" aria-labelledby="tw-18-title" class="group m-auto w-[min(400px,94vw)] p-0 border-0 rounded-[14px] text-center text-tw18-ink bg-tw18-card shadow-[0_30px_60px_-20px_#0f172a47] origin-bottom-right opacity-0 transition-[opacity,translate,rotate,scale,overlay,display] duration-[400ms] ease-[cubic-bezier(.34,1.36,.44,1)] transition-discrete open:opacity-100 open:translate-x-0 open:translate-y-0 open:rotate-0 open:scale-100 open:starting:opacity-0 open:starting:translate-x-6 open:starting:translate-y-8 open:starting:rotate-3 open:starting:scale-[0.96] open:backdrop:bg-tw18-scrim open:backdrop:backdrop-blur-[4px] motion-reduce:transition-[opacity] motion-reduce:duration-150">
<div class="relative px-[30px] pt-8 pb-6">
<button type="button" data-tw18-close aria-label="Close dialog" class="absolute top-3.5 right-3.5 grid place-items-center size-9 rounded-lg border-0 bg-transparent text-[15px] text-tw18-sub cursor-pointer transition-colors duration-200 hover:bg-tw18-well hover:text-tw18-ink focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">✕</button>
<div class="group-data-[view=sent]:hidden">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-tint text-2xl text-tw18-brand">🔑</span>
<h2 id="tw-18-title" class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Reset your password</h2>
<p class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">Enter your email and we'll send a link to reset your password. Link expires in 30 minutes.</p>
<form data-tw18-form class="flex flex-col gap-3">
<label class="flex flex-col gap-1.5 text-left">
<span class="text-xs font-bold uppercase tracking-[0.06em] text-tw18-sub">Email address</span>
<input type="email" autofocus required autocomplete="email" placeholder="you@work.com" class="min-h-11 px-3.5 rounded-lg border border-tw18-line bg-white text-[15px] text-tw18-ink placeholder:text-tw18-sub transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tw18-brand focus:shadow-[0_0_0_3px_var(--color-tw18-ring)] user-invalid:border-tw18-bad">
</label>
<button type="submit" class="min-h-11 mt-1 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white shadow-[0_8px_18px_-6px_var(--color-tw18-glow)] cursor-pointer transition-[background-color,translate] duration-200 hover:bg-tw18-branddark hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px] motion-reduce:hover:translate-y-0">Send reset link →</button>
</form>
</div>
<div hidden class="group-data-[view=sent]:block">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-goodtint text-2xl text-tw18-good">✉</span>
<h3 class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Check your inbox</h3>
<p aria-live="polite" class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">If an account exists for that address, a reset link is on its way. It expires in 30 minutes.</p>
<button type="button" data-tw18-ack data-tw18-close class="w-full min-h-11 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-tw18-branddark focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px]">Got it</button>
</div>
<div class="pt-4 mt-4 border-t border-tw18-line">
<button type="button" data-tw18-close class="min-h-11 px-3 bg-transparent border-0 text-sm font-semibold text-tw18-brand cursor-pointer hover:underline focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">← Return to sign in</button>
<p class="text-xs text-tw18-sub">Can't access this email? <a href="#" class="font-semibold text-tw18-brand underline">Contact support</a></p>
</div>
</div>
</dialog>
</section>@import "tailwindcss";
@theme {
--color-tw18-bg: #f8fafc;
--color-tw18-bg: oklch(0.98 0.005 250);
--color-tw18-card: #ffffff;
--color-tw18-card: oklch(1 0 0);
--color-tw18-well: #f1f5f9;
--color-tw18-well: oklch(0.96 0.006 250);
--color-tw18-ink: #0f172a;
--color-tw18-ink: oklch(0.23 0.04 265);
--color-tw18-sub: #52627a;
--color-tw18-sub: oklch(0.48 0.03 255);
--color-tw18-line: #e2e8f0;
--color-tw18-line: oklch(0.92 0.01 250);
--color-tw18-brand: #2563eb;
--color-tw18-brand: oklch(0.53 0.2 262);
--color-tw18-branddark: #1d4ed8;
--color-tw18-branddark: oklch(0.48 0.2 262);
--color-tw18-tint: #3b82f61f;
--color-tw18-tint: oklch(0.62 0.19 259 / 0.12);
--color-tw18-ring: #3b82f626;
--color-tw18-ring: oklch(0.62 0.19 259 / 0.15);
--color-tw18-glow: #3b82f680;
--color-tw18-glow: oklch(0.62 0.19 259 / 0.5);
--color-tw18-good: #047857;
--color-tw18-good: oklch(0.52 0.12 164);
--color-tw18-goodtint: #0478571f;
--color-tw18-goodtint: oklch(0.52 0.12 164 / 0.12);
--color-tw18-bad: #c81e1e;
--color-tw18-bad: oklch(0.53 0.2 27);
--color-tw18-scrim: #0f172a8c;
--color-tw18-scrim: oklch(0.23 0.04 265 / 0.55);
--font-tw18-sans: 'Outfit', system-ui, sans-serif;
}
.tw-18 {
width: 100%;
}
#tw-18-dlg::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
transition: background .25s ease, backdrop-filter .25s ease, overlay .25s allow-discrete, display .25s allow-discrete;
}
@starting-style {
#tw-18-dlg[open]::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
}
}
@media (prefers-reduced-motion: reduce) {
#tw-18-dlg::backdrop {
transition-duration: .001s;
}
}@import "tailwindcss";
@theme {
--color-tw18-bg: #f8fafc;
--color-tw18-bg: oklch(0.98 0.005 250);
--color-tw18-card: #ffffff;
--color-tw18-card: oklch(1 0 0);
--color-tw18-well: #f1f5f9;
--color-tw18-well: oklch(0.96 0.006 250);
--color-tw18-ink: #0f172a;
--color-tw18-ink: oklch(0.23 0.04 265);
--color-tw18-sub: #52627a;
--color-tw18-sub: oklch(0.48 0.03 255);
--color-tw18-line: #e2e8f0;
--color-tw18-line: oklch(0.92 0.01 250);
--color-tw18-brand: #2563eb;
--color-tw18-brand: oklch(0.53 0.2 262);
--color-tw18-branddark: #1d4ed8;
--color-tw18-branddark: oklch(0.48 0.2 262);
--color-tw18-tint: #3b82f61f;
--color-tw18-tint: oklch(0.62 0.19 259 / 0.12);
--color-tw18-ring: #3b82f626;
--color-tw18-ring: oklch(0.62 0.19 259 / 0.15);
--color-tw18-glow: #3b82f680;
--color-tw18-glow: oklch(0.62 0.19 259 / 0.5);
--color-tw18-good: #047857;
--color-tw18-good: oklch(0.52 0.12 164);
--color-tw18-goodtint: #0478571f;
--color-tw18-goodtint: oklch(0.52 0.12 164 / 0.12);
--color-tw18-bad: #c81e1e;
--color-tw18-bad: oklch(0.53 0.2 27);
--color-tw18-scrim: #0f172a8c;
--color-tw18-scrim: oklch(0.23 0.04 265 / 0.55);
--font-tw18-sans: 'Outfit', system-ui, sans-serif;
}
.tw-18 {
width: 100%;
}
#tw-18-dlg::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
transition: background .25s ease, backdrop-filter .25s ease, overlay .25s allow-discrete, display .25s allow-discrete;
}
@starting-style {
#tw-18-dlg[open]::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
}
}
@media (prefers-reduced-motion: reduce) {
#tw-18-dlg::backdrop {
transition-duration: .001s;
}
}(function () {
var root = document.querySelector('.tw-18');
var dlg = root.querySelector('#tw-18-dlg');
var card = dlg.firstElementChild;
var form = root.querySelector('[data-tw18-form]');
var ack = root.querySelector('[data-tw18-ack]');
form.addEventListener('submit', function (e) {
e.preventDefault();
// Same response either way: never reveal whether the address is registered.
dlg.dataset.view = 'sent';
ack.focus();
});
dlg.addEventListener('close', function () { dlg.dataset.view = 'form'; form.reset(); });
root.querySelector('[data-tw18-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelectorAll('[data-tw18-close]').forEach(function (b) {
b.addEventListener('click', function () { dlg.close(); });
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();(function () {
var root = document.querySelector('.tw-18');
var dlg = root.querySelector('#tw-18-dlg');
var card = dlg.firstElementChild;
var form = root.querySelector('[data-tw18-form]');
var ack = root.querySelector('[data-tw18-ack]');
form.addEventListener('submit', function (e) {
e.preventDefault();
// Same response either way: never reveal whether the address is registered.
dlg.dataset.view = 'sent';
ack.focus();
});
dlg.addEventListener('close', function () { dlg.dataset.view = 'form'; form.reset(); });
root.querySelector('[data-tw18-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelectorAll('[data-tw18-close]').forEach(function (b) {
b.addEventListener('click', function () { dlg.close(); });
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<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-tw18-bg: #f8fafc;
--color-tw18-bg: oklch(0.98 0.005 250);
--color-tw18-card: #ffffff;
--color-tw18-card: oklch(1 0 0);
--color-tw18-well: #f1f5f9;
--color-tw18-well: oklch(0.96 0.006 250);
--color-tw18-ink: #0f172a;
--color-tw18-ink: oklch(0.23 0.04 265);
--color-tw18-sub: #52627a;
--color-tw18-sub: oklch(0.48 0.03 255);
--color-tw18-line: #e2e8f0;
--color-tw18-line: oklch(0.92 0.01 250);
--color-tw18-brand: #2563eb;
--color-tw18-brand: oklch(0.53 0.2 262);
--color-tw18-branddark: #1d4ed8;
--color-tw18-branddark: oklch(0.48 0.2 262);
--color-tw18-tint: #3b82f61f;
--color-tw18-tint: oklch(0.62 0.19 259 / 0.12);
--color-tw18-ring: #3b82f626;
--color-tw18-ring: oklch(0.62 0.19 259 / 0.15);
--color-tw18-glow: #3b82f680;
--color-tw18-glow: oklch(0.62 0.19 259 / 0.5);
--color-tw18-good: #047857;
--color-tw18-good: oklch(0.52 0.12 164);
--color-tw18-goodtint: #0478571f;
--color-tw18-goodtint: oklch(0.52 0.12 164 / 0.12);
--color-tw18-bad: #c81e1e;
--color-tw18-bad: oklch(0.53 0.2 27);
--color-tw18-scrim: #0f172a8c;
--color-tw18-scrim: oklch(0.23 0.04 265 / 0.55);
--font-tw18-sans: 'Outfit', system-ui, sans-serif;
}
.tw-18 {
width: 100%;
}
#tw-18-dlg::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
transition: background .25s ease, backdrop-filter .25s ease, overlay .25s allow-discrete, display .25s allow-discrete;
}
@starting-style {
#tw-18-dlg[open]::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
}
}
@media (prefers-reduced-motion: reduce) {
#tw-18-dlg::backdrop {
transition-duration: .001s;
}
}@import "tailwindcss";
@theme {
--color-tw18-bg: #f8fafc;
--color-tw18-bg: oklch(0.98 0.005 250);
--color-tw18-card: #ffffff;
--color-tw18-card: oklch(1 0 0);
--color-tw18-well: #f1f5f9;
--color-tw18-well: oklch(0.96 0.006 250);
--color-tw18-ink: #0f172a;
--color-tw18-ink: oklch(0.23 0.04 265);
--color-tw18-sub: #52627a;
--color-tw18-sub: oklch(0.48 0.03 255);
--color-tw18-line: #e2e8f0;
--color-tw18-line: oklch(0.92 0.01 250);
--color-tw18-brand: #2563eb;
--color-tw18-brand: oklch(0.53 0.2 262);
--color-tw18-branddark: #1d4ed8;
--color-tw18-branddark: oklch(0.48 0.2 262);
--color-tw18-tint: #3b82f61f;
--color-tw18-tint: oklch(0.62 0.19 259 / 0.12);
--color-tw18-ring: #3b82f626;
--color-tw18-ring: oklch(0.62 0.19 259 / 0.15);
--color-tw18-glow: #3b82f680;
--color-tw18-glow: oklch(0.62 0.19 259 / 0.5);
--color-tw18-good: #047857;
--color-tw18-good: oklch(0.52 0.12 164);
--color-tw18-goodtint: #0478571f;
--color-tw18-goodtint: oklch(0.52 0.12 164 / 0.12);
--color-tw18-bad: #c81e1e;
--color-tw18-bad: oklch(0.53 0.2 27);
--color-tw18-scrim: #0f172a8c;
--color-tw18-scrim: oklch(0.23 0.04 265 / 0.55);
--font-tw18-sans: 'Outfit', system-ui, sans-serif;
}
.tw-18 {
width: 100%;
}
#tw-18-dlg::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
transition: background .25s ease, backdrop-filter .25s ease, overlay .25s allow-discrete, display .25s allow-discrete;
}
@starting-style {
#tw-18-dlg[open]::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
}
}
@media (prefers-reduced-motion: reduce) {
#tw-18-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;800&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;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Demo 01's entrance, unchanged in technique and retuned in palette — which is the argument for keeping entrances as named, reusable patterns rather than one-off keyframes.
- Behaviour change from the CSS original: the success state is implemented, not described. One
data-viewattribute swaps the panels,closeresets it, and the confirmation is announced through a live region. user-invalid:rather thaninvalid:: arequiredfield is invalid from first paint, soinvalid:would redden an untouched form.- The two panels use
hiddenplusgroup-data-[view=sent]:block, so the no-JS default is the form and nothing is visible twice. - UI change from the CSS original: the return link and the field are 44px, the blue was darkened to clear 4.5:1 as body text, and focus moves to the acknowledgement when the view swaps.
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 Password Reset Modal with Enumeration-Safe Confirmation
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-password-reset-modal-corner-slide-spin/
The collection's closing bookend: demo 01's corner swing reused on a warmer palette, proving the entrance library travels. The port adds what the original only warned about — a neutral, enumeration-safe confirmation that swaps in place via one data-view attribute and announces itself to screen readers.
## HTML
```html
<section class="tw-18 w-full min-h-svh grid place-items-center bg-tw18-bg font-tw18-sans px-6 py-10">
<button type="button" data-tw18-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw18-line bg-tw18-card text-[15px] font-semibold text-tw18-brand cursor-pointer transition-[background-color,border-color] duration-200 hover:bg-tw18-well hover:border-tw18-brand focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">Forgot password?</button>
<dialog id="tw-18-dlg" data-view="form" aria-labelledby="tw-18-title" class="group m-auto w-[min(400px,94vw)] p-0 border-0 rounded-[14px] text-center text-tw18-ink bg-tw18-card shadow-[0_30px_60px_-20px_#0f172a47] origin-bottom-right opacity-0 transition-[opacity,translate,rotate,scale,overlay,display] duration-[400ms] ease-[cubic-bezier(.34,1.36,.44,1)] transition-discrete open:opacity-100 open:translate-x-0 open:translate-y-0 open:rotate-0 open:scale-100 open:starting:opacity-0 open:starting:translate-x-6 open:starting:translate-y-8 open:starting:rotate-3 open:starting:scale-[0.96] open:backdrop:bg-tw18-scrim open:backdrop:backdrop-blur-[4px] motion-reduce:transition-[opacity] motion-reduce:duration-150">
<div class="relative px-[30px] pt-8 pb-6">
<button type="button" data-tw18-close aria-label="Close dialog" class="absolute top-3.5 right-3.5 grid place-items-center size-9 rounded-lg border-0 bg-transparent text-[15px] text-tw18-sub cursor-pointer transition-colors duration-200 hover:bg-tw18-well hover:text-tw18-ink focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">✕</button>
<div class="group-data-[view=sent]:hidden">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-tint text-2xl text-tw18-brand">🔑</span>
<h2 id="tw-18-title" class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Reset your password</h2>
<p class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">Enter your email and we'll send a link to reset your password. Link expires in 30 minutes.</p>
<form data-tw18-form class="flex flex-col gap-3">
<label class="flex flex-col gap-1.5 text-left">
<span class="text-xs font-bold uppercase tracking-[0.06em] text-tw18-sub">Email address</span>
<input type="email" autofocus required autocomplete="email" placeholder="you@work.com" class="min-h-11 px-3.5 rounded-lg border border-tw18-line bg-white text-[15px] text-tw18-ink placeholder:text-tw18-sub transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tw18-brand focus:shadow-[0_0_0_3px_var(--color-tw18-ring)] user-invalid:border-tw18-bad">
</label>
<button type="submit" class="min-h-11 mt-1 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white shadow-[0_8px_18px_-6px_var(--color-tw18-glow)] cursor-pointer transition-[background-color,translate] duration-200 hover:bg-tw18-branddark hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px] motion-reduce:hover:translate-y-0">Send reset link →</button>
</form>
</div>
<div hidden class="group-data-[view=sent]:block">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-goodtint text-2xl text-tw18-good">✉</span>
<h3 class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Check your inbox</h3>
<p aria-live="polite" class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">If an account exists for that address, a reset link is on its way. It expires in 30 minutes.</p>
<button type="button" data-tw18-ack data-tw18-close class="w-full min-h-11 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-tw18-branddark focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px]">Got it</button>
</div>
<div class="pt-4 mt-4 border-t border-tw18-line">
<button type="button" data-tw18-close class="min-h-11 px-3 bg-transparent border-0 text-sm font-semibold text-tw18-brand cursor-pointer hover:underline focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">← Return to sign in</button>
<p class="text-xs text-tw18-sub">Can't access this email? <a href="#" class="font-semibold text-tw18-brand underline">Contact support</a></p>
</div>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tw18-bg: #f8fafc;
--color-tw18-bg: oklch(0.98 0.005 250);
--color-tw18-card: #ffffff;
--color-tw18-card: oklch(1 0 0);
--color-tw18-well: #f1f5f9;
--color-tw18-well: oklch(0.96 0.006 250);
--color-tw18-ink: #0f172a;
--color-tw18-ink: oklch(0.23 0.04 265);
--color-tw18-sub: #52627a;
--color-tw18-sub: oklch(0.48 0.03 255);
--color-tw18-line: #e2e8f0;
--color-tw18-line: oklch(0.92 0.01 250);
--color-tw18-brand: #2563eb;
--color-tw18-brand: oklch(0.53 0.2 262);
--color-tw18-branddark: #1d4ed8;
--color-tw18-branddark: oklch(0.48 0.2 262);
--color-tw18-tint: #3b82f61f;
--color-tw18-tint: oklch(0.62 0.19 259 / 0.12);
--color-tw18-ring: #3b82f626;
--color-tw18-ring: oklch(0.62 0.19 259 / 0.15);
--color-tw18-glow: #3b82f680;
--color-tw18-glow: oklch(0.62 0.19 259 / 0.5);
--color-tw18-good: #047857;
--color-tw18-good: oklch(0.52 0.12 164);
--color-tw18-goodtint: #0478571f;
--color-tw18-goodtint: oklch(0.52 0.12 164 / 0.12);
--color-tw18-bad: #c81e1e;
--color-tw18-bad: oklch(0.53 0.2 27);
--color-tw18-scrim: #0f172a8c;
--color-tw18-scrim: oklch(0.23 0.04 265 / 0.55);
--font-tw18-sans: 'Outfit', system-ui, sans-serif;
}
.tw-18 {
width: 100%;
}
#tw-18-dlg::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
transition: background .25s ease, backdrop-filter .25s ease, overlay .25s allow-discrete, display .25s allow-discrete;
}
@starting-style {
#tw-18-dlg[open]::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
}
}
@media (prefers-reduced-motion: reduce) {
#tw-18-dlg::backdrop {
transition-duration: .001s;
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tw-18');
var dlg = root.querySelector('#tw-18-dlg');
var card = dlg.firstElementChild;
var form = root.querySelector('[data-tw18-form]');
var ack = root.querySelector('[data-tw18-ack]');
form.addEventListener('submit', function (e) {
e.preventDefault();
// Same response either way: never reveal whether the address is registered.
dlg.dataset.view = 'sent';
ack.focus();
});
dlg.addEventListener('close', function () { dlg.dataset.view = 'form'; form.reset(); });
root.querySelector('[data-tw18-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelectorAll('[data-tw18-close]').forEach(function (b) {
b.addEventListener('click', function () { dlg.close(); });
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();
```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 Password Reset Modal with Enumeration-Safe Confirmation
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-password-reset-modal-corner-slide-spin/
The collection's closing bookend: demo 01's corner swing reused on a warmer palette, proving the entrance library travels. The port adds what the original only warned about — a neutral, enumeration-safe confirmation that swaps in place via one data-view attribute and announces itself to screen readers.
## HTML
```html
<section class="tw-18 w-full min-h-svh grid place-items-center bg-tw18-bg font-tw18-sans px-6 py-10">
<button type="button" data-tw18-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-lg border border-tw18-line bg-tw18-card text-[15px] font-semibold text-tw18-brand cursor-pointer transition-[background-color,border-color] duration-200 hover:bg-tw18-well hover:border-tw18-brand focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">Forgot password?</button>
<dialog id="tw-18-dlg" data-view="form" aria-labelledby="tw-18-title" class="group m-auto w-[min(400px,94vw)] p-0 border-0 rounded-[14px] text-center text-tw18-ink bg-tw18-card shadow-[0_30px_60px_-20px_#0f172a47] origin-bottom-right opacity-0 transition-[opacity,translate,rotate,scale,overlay,display] duration-[400ms] ease-[cubic-bezier(.34,1.36,.44,1)] transition-discrete open:opacity-100 open:translate-x-0 open:translate-y-0 open:rotate-0 open:scale-100 open:starting:opacity-0 open:starting:translate-x-6 open:starting:translate-y-8 open:starting:rotate-3 open:starting:scale-[0.96] open:backdrop:bg-tw18-scrim open:backdrop:backdrop-blur-[4px] motion-reduce:transition-[opacity] motion-reduce:duration-150">
<div class="relative px-[30px] pt-8 pb-6">
<button type="button" data-tw18-close aria-label="Close dialog" class="absolute top-3.5 right-3.5 grid place-items-center size-9 rounded-lg border-0 bg-transparent text-[15px] text-tw18-sub cursor-pointer transition-colors duration-200 hover:bg-tw18-well hover:text-tw18-ink focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">✕</button>
<div class="group-data-[view=sent]:hidden">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-tint text-2xl text-tw18-brand">🔑</span>
<h2 id="tw-18-title" class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Reset your password</h2>
<p class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">Enter your email and we'll send a link to reset your password. Link expires in 30 minutes.</p>
<form data-tw18-form class="flex flex-col gap-3">
<label class="flex flex-col gap-1.5 text-left">
<span class="text-xs font-bold uppercase tracking-[0.06em] text-tw18-sub">Email address</span>
<input type="email" autofocus required autocomplete="email" placeholder="you@work.com" class="min-h-11 px-3.5 rounded-lg border border-tw18-line bg-white text-[15px] text-tw18-ink placeholder:text-tw18-sub transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tw18-brand focus:shadow-[0_0_0_3px_var(--color-tw18-ring)] user-invalid:border-tw18-bad">
</label>
<button type="submit" class="min-h-11 mt-1 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white shadow-[0_8px_18px_-6px_var(--color-tw18-glow)] cursor-pointer transition-[background-color,translate] duration-200 hover:bg-tw18-branddark hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px] motion-reduce:hover:translate-y-0">Send reset link →</button>
</form>
</div>
<div hidden class="group-data-[view=sent]:block">
<span aria-hidden="true" class="inline-grid place-items-center size-13 mb-3.5 rounded-full bg-tw18-goodtint text-2xl text-tw18-good">✉</span>
<h3 class="mb-2 text-[1.35rem] font-extrabold tracking-[-0.015em] leading-tight">Check your inbox</h3>
<p aria-live="polite" class="mb-5 text-sm leading-relaxed text-tw18-sub text-pretty">If an account exists for that address, a reset link is on its way. It expires in 30 minutes.</p>
<button type="button" data-tw18-ack data-tw18-close class="w-full min-h-11 px-5 rounded-lg border-0 bg-tw18-brand text-[15px] font-bold text-white cursor-pointer transition-colors duration-200 hover:bg-tw18-branddark focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-[3px]">Got it</button>
</div>
<div class="pt-4 mt-4 border-t border-tw18-line">
<button type="button" data-tw18-close class="min-h-11 px-3 bg-transparent border-0 text-sm font-semibold text-tw18-brand cursor-pointer hover:underline focus-visible:outline-2 focus-visible:outline-tw18-brand focus-visible:outline-offset-2">← Return to sign in</button>
<p class="text-xs text-tw18-sub">Can't access this email? <a href="#" class="font-semibold text-tw18-brand underline">Contact support</a></p>
</div>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tw18-bg: #f8fafc;
--color-tw18-bg: oklch(0.98 0.005 250);
--color-tw18-card: #ffffff;
--color-tw18-card: oklch(1 0 0);
--color-tw18-well: #f1f5f9;
--color-tw18-well: oklch(0.96 0.006 250);
--color-tw18-ink: #0f172a;
--color-tw18-ink: oklch(0.23 0.04 265);
--color-tw18-sub: #52627a;
--color-tw18-sub: oklch(0.48 0.03 255);
--color-tw18-line: #e2e8f0;
--color-tw18-line: oklch(0.92 0.01 250);
--color-tw18-brand: #2563eb;
--color-tw18-brand: oklch(0.53 0.2 262);
--color-tw18-branddark: #1d4ed8;
--color-tw18-branddark: oklch(0.48 0.2 262);
--color-tw18-tint: #3b82f61f;
--color-tw18-tint: oklch(0.62 0.19 259 / 0.12);
--color-tw18-ring: #3b82f626;
--color-tw18-ring: oklch(0.62 0.19 259 / 0.15);
--color-tw18-glow: #3b82f680;
--color-tw18-glow: oklch(0.62 0.19 259 / 0.5);
--color-tw18-good: #047857;
--color-tw18-good: oklch(0.52 0.12 164);
--color-tw18-goodtint: #0478571f;
--color-tw18-goodtint: oklch(0.52 0.12 164 / 0.12);
--color-tw18-bad: #c81e1e;
--color-tw18-bad: oklch(0.53 0.2 27);
--color-tw18-scrim: #0f172a8c;
--color-tw18-scrim: oklch(0.23 0.04 265 / 0.55);
--font-tw18-sans: 'Outfit', system-ui, sans-serif;
}
.tw-18 {
width: 100%;
}
#tw-18-dlg::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
transition: background .25s ease, backdrop-filter .25s ease, overlay .25s allow-discrete, display .25s allow-discrete;
}
@starting-style {
#tw-18-dlg[open]::backdrop {
background: oklch(0.23 0.04 265 / 0);
backdrop-filter: blur(0px);
}
}
@media (prefers-reduced-motion: reduce) {
#tw-18-dlg::backdrop {
transition-duration: .001s;
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tw-18');
var dlg = root.querySelector('#tw-18-dlg');
var card = dlg.firstElementChild;
var form = root.querySelector('[data-tw18-form]');
var ack = root.querySelector('[data-tw18-ack]');
form.addEventListener('submit', function (e) {
e.preventDefault();
// Same response either way: never reveal whether the address is registered.
dlg.dataset.view = 'sent';
ack.focus();
});
dlg.addEventListener('close', function () { dlg.dataset.view = 'form'; form.reset(); });
root.querySelector('[data-tw18-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelectorAll('[data-tw18-close]').forEach(function (b) {
b.addEventListener('click', function () { dlg.close(); });
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();
```How this works
The dialog holds its own state. data-view starts at form and becomes sent on submit; both panels live in the markup and read that attribute, and close resets it so the next open starts clean.
The message never confirms an account. "If an account exists for that address, a reset link is on its way" is the same sentence whether or not the address is registered — anything more specific is an enumeration oracle.
Warm, not bouncy. A locked-out user is already frustrated; the 3° swing reads as help arriving, where an elastic overshoot would read as flippant.
Make it yours
- Offer a magic link beside the reset — for many products it removes the password from the flow entirely.
- Add a resend cooldown with a visible countdown and a
disabled:styled button. - Swap to SMS with
type="tel"andautocomplete="tel"for phone-first products. - Put an SSO option above the field for enterprise tenants, where a reset is the wrong path altogether.
Gotchas — read before shipping
- Never say whether the address is registered. A specific "no such account" message hands an attacker a user list.
- Rate-limit requests server-side; without a cap, the form is an inbox-flooding tool aimed at your own users.
- Reset tokens must expire and be single-use, or an intercepted email stays exploitable indefinitely.
- Reset
data-viewonclose, or reopening lands the user on last time's confirmation. - Move focus to the acknowledge button when the view swaps — a keyboard user whose focus was inside the now-hidden form is otherwise stranded on the body.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ · 117+ anim | 16.4+ · 17.5+ anim | 128+ · 129+ anim | 111+ · 117+ anim |
:user-invalid is Baseline 2023; where missing the field simply never reddens and native validation still blocks submit.