16 / 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 Contact Support Modal with Live Validation
A support form that opens over the dashboard instead of navigating away: two-column identity fields, a topic select, priority radios and a counted message box. Validation styling uses user-invalid:, so a field only turns red after the user has actually left it wrong.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tm-16 w-full min-h-svh grid place-items-center bg-tm16-bg font-tm16-sans px-6 py-10">
<button type="button" data-tm16-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm16-ink bg-tm16-btn border border-tm16-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover hover:border-tm16-acc focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">Contact support</button>
<dialog id="tm-16-dlg" data-state="form" aria-labelledby="tm-16-title" class="group m-auto w-[min(520px,94vw)] max-h-[92dvh] overflow-auto p-0 border-0 rounded-[20px] text-tm16-ink bg-tm16-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm16-edge)] opacity-0 translate-y-3 scale-[0.97] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:translate-y-0 open:scale-100 open:starting:opacity-0 open:starting:translate-y-3 open:starting:scale-[0.97] open:backdrop:bg-tm16-scrim open:backdrop:backdrop-blur-[5px] motion-reduce:transition-none">
<div class="p-7">
<header class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 id="tm-16-title" class="text-[20px] font-bold text-tm16-hi">Contact support</h2>
<p class="mt-1 text-[13px] text-tm16-mut">We usually reply within a few hours.</p>
</div>
<button type="button" data-tm16-close aria-label="Close" class="grid place-items-center size-11 shrink-0 rounded-lg border-0 bg-transparent text-tm16-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm16-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">×</button>
</header>
<form data-tm16-form novalidate class="flex flex-col gap-4 group-data-[state=sent]:hidden">
<div class="grid grid-cols-2 gap-3 max-[420px]:grid-cols-1">
<div>
<label for="tm-16-name" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Name</label>
<input id="tm-16-name" type="text" required autocomplete="name" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Please enter your name.</p>
</div>
<div>
<label for="tm-16-email" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Email</label>
<input id="tm-16-email" type="email" required autocomplete="email" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Enter a valid email address.</p>
</div>
</div>
<div>
<label for="tm-16-topic" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Topic</label>
<select id="tm-16-topic" required class="w-full min-h-11 px-3 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi cursor-pointer transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<option value="">Choose a topic…</option>
<option>Billing & plans</option>
<option>Technical issue</option>
<option>Feature request</option>
<option>Something else</option>
</select>
</div>
<fieldset class="border-0 p-0">
<legend class="mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Priority</legend>
<div class="flex flex-wrap gap-4">
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" checked class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Low</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Normal</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Urgent</label>
</div>
</fieldset>
<div>
<label for="tm-16-msg" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Message</label>
<textarea id="tm-16-msg" data-tm16-msg required maxlength="500" rows="4" placeholder="Describe what's happening…" class="peer w-full min-h-28 max-h-60 p-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm leading-relaxed text-tm16-hi placeholder:text-tm16-faint resize-y [field-sizing:content] transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad"></textarea>
<div class="flex justify-between mt-1">
<p class="hidden text-[11.5px] text-tm16-bad peer-user-invalid:block">Tell us what's happening.</p>
<p class="ml-auto text-[11.5px] text-tm16-faint"><b data-tm16-count class="text-tm16-mut">0</b>/500</p>
</div>
</div>
<div class="grid grid-cols-[auto_1fr] gap-3 mt-1">
<button type="button" data-tm16-cancel class="min-h-12 px-5 rounded-xl border border-tm16-btnedge bg-tm16-btn text-sm font-semibold text-tm16-ink cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Cancel</button>
<button type="submit" class="min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter,translate] duration-200 hover:brightness-110 active:translate-y-px focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 motion-reduce:active:translate-y-0">Send message</button>
</div>
</form>
<div role="status" class="hidden text-center group-data-[state=sent]:block">
<div aria-hidden="true" class="grid place-items-center size-14 mx-auto mb-4 rounded-2xl text-2xl bg-tm16-oktint text-tm16-ok shadow-[inset_0_0_0_1px_var(--color-tm16-okedge)]">✓</div>
<h3 class="mb-2 text-lg font-bold text-tm16-hi">Message sent</h3>
<p class="mb-5 text-sm leading-relaxed text-tm16-mut">Thanks — we've logged your request and emailed a copy to your inbox.</p>
<button type="button" data-tm16-done class="w-full min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Close</button>
</div>
</div>
</dialog>
</section><section class="tm-16 w-full min-h-svh grid place-items-center bg-tm16-bg font-tm16-sans px-6 py-10">
<button type="button" data-tm16-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm16-ink bg-tm16-btn border border-tm16-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover hover:border-tm16-acc focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">Contact support</button>
<dialog id="tm-16-dlg" data-state="form" aria-labelledby="tm-16-title" class="group m-auto w-[min(520px,94vw)] max-h-[92dvh] overflow-auto p-0 border-0 rounded-[20px] text-tm16-ink bg-tm16-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm16-edge)] opacity-0 translate-y-3 scale-[0.97] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:translate-y-0 open:scale-100 open:starting:opacity-0 open:starting:translate-y-3 open:starting:scale-[0.97] open:backdrop:bg-tm16-scrim open:backdrop:backdrop-blur-[5px] motion-reduce:transition-none">
<div class="p-7">
<header class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 id="tm-16-title" class="text-[20px] font-bold text-tm16-hi">Contact support</h2>
<p class="mt-1 text-[13px] text-tm16-mut">We usually reply within a few hours.</p>
</div>
<button type="button" data-tm16-close aria-label="Close" class="grid place-items-center size-11 shrink-0 rounded-lg border-0 bg-transparent text-tm16-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm16-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">×</button>
</header>
<form data-tm16-form novalidate class="flex flex-col gap-4 group-data-[state=sent]:hidden">
<div class="grid grid-cols-2 gap-3 max-[420px]:grid-cols-1">
<div>
<label for="tm-16-name" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Name</label>
<input id="tm-16-name" type="text" required autocomplete="name" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Please enter your name.</p>
</div>
<div>
<label for="tm-16-email" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Email</label>
<input id="tm-16-email" type="email" required autocomplete="email" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Enter a valid email address.</p>
</div>
</div>
<div>
<label for="tm-16-topic" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Topic</label>
<select id="tm-16-topic" required class="w-full min-h-11 px-3 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi cursor-pointer transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<option value="">Choose a topic…</option>
<option>Billing & plans</option>
<option>Technical issue</option>
<option>Feature request</option>
<option>Something else</option>
</select>
</div>
<fieldset class="border-0 p-0">
<legend class="mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Priority</legend>
<div class="flex flex-wrap gap-4">
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" checked class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Low</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Normal</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Urgent</label>
</div>
</fieldset>
<div>
<label for="tm-16-msg" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Message</label>
<textarea id="tm-16-msg" data-tm16-msg required maxlength="500" rows="4" placeholder="Describe what's happening…" class="peer w-full min-h-28 max-h-60 p-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm leading-relaxed text-tm16-hi placeholder:text-tm16-faint resize-y [field-sizing:content] transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad"></textarea>
<div class="flex justify-between mt-1">
<p class="hidden text-[11.5px] text-tm16-bad peer-user-invalid:block">Tell us what's happening.</p>
<p class="ml-auto text-[11.5px] text-tm16-faint"><b data-tm16-count class="text-tm16-mut">0</b>/500</p>
</div>
</div>
<div class="grid grid-cols-[auto_1fr] gap-3 mt-1">
<button type="button" data-tm16-cancel class="min-h-12 px-5 rounded-xl border border-tm16-btnedge bg-tm16-btn text-sm font-semibold text-tm16-ink cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Cancel</button>
<button type="submit" class="min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter,translate] duration-200 hover:brightness-110 active:translate-y-px focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 motion-reduce:active:translate-y-0">Send message</button>
</div>
</form>
<div role="status" class="hidden text-center group-data-[state=sent]:block">
<div aria-hidden="true" class="grid place-items-center size-14 mx-auto mb-4 rounded-2xl text-2xl bg-tm16-oktint text-tm16-ok shadow-[inset_0_0_0_1px_var(--color-tm16-okedge)]">✓</div>
<h3 class="mb-2 text-lg font-bold text-tm16-hi">Message sent</h3>
<p class="mb-5 text-sm leading-relaxed text-tm16-mut">Thanks — we've logged your request and emailed a copy to your inbox.</p>
<button type="button" data-tm16-done class="w-full min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Close</button>
</div>
</div>
</dialog>
</section>@import "tailwindcss";
@theme {
--color-tm16-bg: #08080d;
--color-tm16-bg: oklch(0.14 0.012 275);
--color-tm16-card: #15161f;
--color-tm16-card: oklch(0.21 0.015 275);
--color-tm16-well: #0e0f18;
--color-tm16-well: oklch(0.17 0.013 275);
--color-tm16-btn: #1a1c2a;
--color-tm16-btn: oklch(0.23 0.02 275);
--color-tm16-btnhover: #232537;
--color-tm16-btnhover: oklch(0.27 0.028 275);
--color-tm16-btnedge: #333552;
--color-tm16-btnedge: oklch(0.32 0.03 275);
--color-tm16-edge: #2a2c42;
--color-tm16-edge: oklch(0.29 0.025 275);
--color-tm16-hi: #f2f2fb;
--color-tm16-hi: oklch(0.96 0.008 275);
--color-tm16-ink: #dcdcec;
--color-tm16-ink: oklch(0.89 0.015 275);
--color-tm16-mut: #9c9cba;
--color-tm16-mut: oklch(0.71 0.025 275);
--color-tm16-faint: #7c7c9a;
--color-tm16-faint: oklch(0.6 0.03 275);
--color-tm16-acc: #8aa0ff;
--color-tm16-acc: oklch(0.73 0.15 272);
--color-tm16-onacc: #070a1f;
--color-tm16-onacc: oklch(0.17 0.05 272);
--color-tm16-ring: #8aa0ff3d;
--color-tm16-ring: oklch(0.73 0.15 272 / 0.24);
--color-tm16-bad: #f08a8a;
--color-tm16-bad: oklch(0.72 0.15 22);
--color-tm16-ok: #7ae2a8;
--color-tm16-ok: oklch(0.85 0.16 155);
--color-tm16-oktint: #142a1f;
--color-tm16-oktint: oklch(0.25 0.05 155);
--color-tm16-okedge: #37785a;
--color-tm16-okedge: oklch(0.48 0.09 155);
--color-tm16-scrim: #08080da3;
--color-tm16-scrim: oklch(0.12 0.01 275 / 0.64);
--font-tm16-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-16 {
width: 100%;
}
#tm-16-dlg::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-16-dlg[open]::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
}
}@import "tailwindcss";
@theme {
--color-tm16-bg: #08080d;
--color-tm16-bg: oklch(0.14 0.012 275);
--color-tm16-card: #15161f;
--color-tm16-card: oklch(0.21 0.015 275);
--color-tm16-well: #0e0f18;
--color-tm16-well: oklch(0.17 0.013 275);
--color-tm16-btn: #1a1c2a;
--color-tm16-btn: oklch(0.23 0.02 275);
--color-tm16-btnhover: #232537;
--color-tm16-btnhover: oklch(0.27 0.028 275);
--color-tm16-btnedge: #333552;
--color-tm16-btnedge: oklch(0.32 0.03 275);
--color-tm16-edge: #2a2c42;
--color-tm16-edge: oklch(0.29 0.025 275);
--color-tm16-hi: #f2f2fb;
--color-tm16-hi: oklch(0.96 0.008 275);
--color-tm16-ink: #dcdcec;
--color-tm16-ink: oklch(0.89 0.015 275);
--color-tm16-mut: #9c9cba;
--color-tm16-mut: oklch(0.71 0.025 275);
--color-tm16-faint: #7c7c9a;
--color-tm16-faint: oklch(0.6 0.03 275);
--color-tm16-acc: #8aa0ff;
--color-tm16-acc: oklch(0.73 0.15 272);
--color-tm16-onacc: #070a1f;
--color-tm16-onacc: oklch(0.17 0.05 272);
--color-tm16-ring: #8aa0ff3d;
--color-tm16-ring: oklch(0.73 0.15 272 / 0.24);
--color-tm16-bad: #f08a8a;
--color-tm16-bad: oklch(0.72 0.15 22);
--color-tm16-ok: #7ae2a8;
--color-tm16-ok: oklch(0.85 0.16 155);
--color-tm16-oktint: #142a1f;
--color-tm16-oktint: oklch(0.25 0.05 155);
--color-tm16-okedge: #37785a;
--color-tm16-okedge: oklch(0.48 0.09 155);
--color-tm16-scrim: #08080da3;
--color-tm16-scrim: oklch(0.12 0.01 275 / 0.64);
--font-tm16-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-16 {
width: 100%;
}
#tm-16-dlg::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-16-dlg[open]::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
}
}(function () {
var root = document.querySelector('.tm-16');
var dlg = root.querySelector('#tm-16-dlg');
var form = root.querySelector('[data-tm16-form]');
var msg = root.querySelector('[data-tm16-msg]');
var count = root.querySelector('[data-tm16-count]');
root.querySelector('[data-tm16-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm16-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-cancel]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-done]').addEventListener('click', function () { dlg.close(); });
msg.addEventListener('input', function () { count.textContent = msg.value.length; });
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!form.checkValidity()) { form.reportValidity(); return; } // native constraints first
dlg.dataset.state = 'sent'; // values are never cleared
root.querySelector('[data-tm16-done]').focus();
});
dlg.addEventListener('close', function () { dlg.dataset.state = 'form'; });
})();(function () {
var root = document.querySelector('.tm-16');
var dlg = root.querySelector('#tm-16-dlg');
var form = root.querySelector('[data-tm16-form]');
var msg = root.querySelector('[data-tm16-msg]');
var count = root.querySelector('[data-tm16-count]');
root.querySelector('[data-tm16-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm16-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-cancel]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-done]').addEventListener('click', function () { dlg.close(); });
msg.addEventListener('input', function () { count.textContent = msg.value.length; });
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!form.checkValidity()) { form.reportValidity(); return; } // native constraints first
dlg.dataset.state = 'sent'; // values are never cleared
root.querySelector('[data-tm16-done]').focus();
});
dlg.addEventListener('close', function () { dlg.dataset.state = 'form'; });
})();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-tm16-bg: #08080d;
--color-tm16-bg: oklch(0.14 0.012 275);
--color-tm16-card: #15161f;
--color-tm16-card: oklch(0.21 0.015 275);
--color-tm16-well: #0e0f18;
--color-tm16-well: oklch(0.17 0.013 275);
--color-tm16-btn: #1a1c2a;
--color-tm16-btn: oklch(0.23 0.02 275);
--color-tm16-btnhover: #232537;
--color-tm16-btnhover: oklch(0.27 0.028 275);
--color-tm16-btnedge: #333552;
--color-tm16-btnedge: oklch(0.32 0.03 275);
--color-tm16-edge: #2a2c42;
--color-tm16-edge: oklch(0.29 0.025 275);
--color-tm16-hi: #f2f2fb;
--color-tm16-hi: oklch(0.96 0.008 275);
--color-tm16-ink: #dcdcec;
--color-tm16-ink: oklch(0.89 0.015 275);
--color-tm16-mut: #9c9cba;
--color-tm16-mut: oklch(0.71 0.025 275);
--color-tm16-faint: #7c7c9a;
--color-tm16-faint: oklch(0.6 0.03 275);
--color-tm16-acc: #8aa0ff;
--color-tm16-acc: oklch(0.73 0.15 272);
--color-tm16-onacc: #070a1f;
--color-tm16-onacc: oklch(0.17 0.05 272);
--color-tm16-ring: #8aa0ff3d;
--color-tm16-ring: oklch(0.73 0.15 272 / 0.24);
--color-tm16-bad: #f08a8a;
--color-tm16-bad: oklch(0.72 0.15 22);
--color-tm16-ok: #7ae2a8;
--color-tm16-ok: oklch(0.85 0.16 155);
--color-tm16-oktint: #142a1f;
--color-tm16-oktint: oklch(0.25 0.05 155);
--color-tm16-okedge: #37785a;
--color-tm16-okedge: oklch(0.48 0.09 155);
--color-tm16-scrim: #08080da3;
--color-tm16-scrim: oklch(0.12 0.01 275 / 0.64);
--font-tm16-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-16 {
width: 100%;
}
#tm-16-dlg::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-16-dlg[open]::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
}
}@import "tailwindcss";
@theme {
--color-tm16-bg: #08080d;
--color-tm16-bg: oklch(0.14 0.012 275);
--color-tm16-card: #15161f;
--color-tm16-card: oklch(0.21 0.015 275);
--color-tm16-well: #0e0f18;
--color-tm16-well: oklch(0.17 0.013 275);
--color-tm16-btn: #1a1c2a;
--color-tm16-btn: oklch(0.23 0.02 275);
--color-tm16-btnhover: #232537;
--color-tm16-btnhover: oklch(0.27 0.028 275);
--color-tm16-btnedge: #333552;
--color-tm16-btnedge: oklch(0.32 0.03 275);
--color-tm16-edge: #2a2c42;
--color-tm16-edge: oklch(0.29 0.025 275);
--color-tm16-hi: #f2f2fb;
--color-tm16-hi: oklch(0.96 0.008 275);
--color-tm16-ink: #dcdcec;
--color-tm16-ink: oklch(0.89 0.015 275);
--color-tm16-mut: #9c9cba;
--color-tm16-mut: oklch(0.71 0.025 275);
--color-tm16-faint: #7c7c9a;
--color-tm16-faint: oklch(0.6 0.03 275);
--color-tm16-acc: #8aa0ff;
--color-tm16-acc: oklch(0.73 0.15 272);
--color-tm16-onacc: #070a1f;
--color-tm16-onacc: oklch(0.17 0.05 272);
--color-tm16-ring: #8aa0ff3d;
--color-tm16-ring: oklch(0.73 0.15 272 / 0.24);
--color-tm16-bad: #f08a8a;
--color-tm16-bad: oklch(0.72 0.15 22);
--color-tm16-ok: #7ae2a8;
--color-tm16-ok: oklch(0.85 0.16 155);
--color-tm16-oktint: #142a1f;
--color-tm16-oktint: oklch(0.25 0.05 155);
--color-tm16-okedge: #37785a;
--color-tm16-okedge: oklch(0.48 0.09 155);
--color-tm16-scrim: #08080da3;
--color-tm16-scrim: oklch(0.12 0.01 275 / 0.64);
--font-tm16-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-16 {
width: 100%;
}
#tm-16-dlg::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-16-dlg[open]::backdrop {
background: oklch(0.12 0.01 275 / 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&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&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The port's substantive change is
user-invalid:over the original's reliance on browser bubbles alone: each field now carries its own inline hint, revealed bypeer-user-invalid:, and never before the user has interacted. [field-sizing:content]next tomin-h-28 max-h-60is progressive enhancement written in one class list — Chromium grows the box, everyone else gets a sensible fixed range.- UI change from the CSS original: inline per-field error messages, a 44px minimum on every input and radio row, a two-column identity row that collapses under 420px, and a success panel that takes focus rather than only being marked
role="status".
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 Contact Support Modal with Live Validation
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-contact-support-modal-live-validation/
A support form that opens over the dashboard instead of navigating away: two-column identity fields, a topic select, priority radios and a counted message box. Validation styling uses user-invalid:, so a field only turns red after the user has actually left it wrong.
## HTML
```html
<section class="tm-16 w-full min-h-svh grid place-items-center bg-tm16-bg font-tm16-sans px-6 py-10">
<button type="button" data-tm16-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm16-ink bg-tm16-btn border border-tm16-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover hover:border-tm16-acc focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">Contact support</button>
<dialog id="tm-16-dlg" data-state="form" aria-labelledby="tm-16-title" class="group m-auto w-[min(520px,94vw)] max-h-[92dvh] overflow-auto p-0 border-0 rounded-[20px] text-tm16-ink bg-tm16-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm16-edge)] opacity-0 translate-y-3 scale-[0.97] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:translate-y-0 open:scale-100 open:starting:opacity-0 open:starting:translate-y-3 open:starting:scale-[0.97] open:backdrop:bg-tm16-scrim open:backdrop:backdrop-blur-[5px] motion-reduce:transition-none">
<div class="p-7">
<header class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 id="tm-16-title" class="text-[20px] font-bold text-tm16-hi">Contact support</h2>
<p class="mt-1 text-[13px] text-tm16-mut">We usually reply within a few hours.</p>
</div>
<button type="button" data-tm16-close aria-label="Close" class="grid place-items-center size-11 shrink-0 rounded-lg border-0 bg-transparent text-tm16-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm16-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">×</button>
</header>
<form data-tm16-form novalidate class="flex flex-col gap-4 group-data-[state=sent]:hidden">
<div class="grid grid-cols-2 gap-3 max-[420px]:grid-cols-1">
<div>
<label for="tm-16-name" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Name</label>
<input id="tm-16-name" type="text" required autocomplete="name" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Please enter your name.</p>
</div>
<div>
<label for="tm-16-email" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Email</label>
<input id="tm-16-email" type="email" required autocomplete="email" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Enter a valid email address.</p>
</div>
</div>
<div>
<label for="tm-16-topic" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Topic</label>
<select id="tm-16-topic" required class="w-full min-h-11 px-3 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi cursor-pointer transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<option value="">Choose a topic…</option>
<option>Billing & plans</option>
<option>Technical issue</option>
<option>Feature request</option>
<option>Something else</option>
</select>
</div>
<fieldset class="border-0 p-0">
<legend class="mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Priority</legend>
<div class="flex flex-wrap gap-4">
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" checked class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Low</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Normal</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Urgent</label>
</div>
</fieldset>
<div>
<label for="tm-16-msg" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Message</label>
<textarea id="tm-16-msg" data-tm16-msg required maxlength="500" rows="4" placeholder="Describe what's happening…" class="peer w-full min-h-28 max-h-60 p-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm leading-relaxed text-tm16-hi placeholder:text-tm16-faint resize-y [field-sizing:content] transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad"></textarea>
<div class="flex justify-between mt-1">
<p class="hidden text-[11.5px] text-tm16-bad peer-user-invalid:block">Tell us what's happening.</p>
<p class="ml-auto text-[11.5px] text-tm16-faint"><b data-tm16-count class="text-tm16-mut">0</b>/500</p>
</div>
</div>
<div class="grid grid-cols-[auto_1fr] gap-3 mt-1">
<button type="button" data-tm16-cancel class="min-h-12 px-5 rounded-xl border border-tm16-btnedge bg-tm16-btn text-sm font-semibold text-tm16-ink cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Cancel</button>
<button type="submit" class="min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter,translate] duration-200 hover:brightness-110 active:translate-y-px focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 motion-reduce:active:translate-y-0">Send message</button>
</div>
</form>
<div role="status" class="hidden text-center group-data-[state=sent]:block">
<div aria-hidden="true" class="grid place-items-center size-14 mx-auto mb-4 rounded-2xl text-2xl bg-tm16-oktint text-tm16-ok shadow-[inset_0_0_0_1px_var(--color-tm16-okedge)]">✓</div>
<h3 class="mb-2 text-lg font-bold text-tm16-hi">Message sent</h3>
<p class="mb-5 text-sm leading-relaxed text-tm16-mut">Thanks — we've logged your request and emailed a copy to your inbox.</p>
<button type="button" data-tm16-done class="w-full min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Close</button>
</div>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm16-bg: #08080d;
--color-tm16-bg: oklch(0.14 0.012 275);
--color-tm16-card: #15161f;
--color-tm16-card: oklch(0.21 0.015 275);
--color-tm16-well: #0e0f18;
--color-tm16-well: oklch(0.17 0.013 275);
--color-tm16-btn: #1a1c2a;
--color-tm16-btn: oklch(0.23 0.02 275);
--color-tm16-btnhover: #232537;
--color-tm16-btnhover: oklch(0.27 0.028 275);
--color-tm16-btnedge: #333552;
--color-tm16-btnedge: oklch(0.32 0.03 275);
--color-tm16-edge: #2a2c42;
--color-tm16-edge: oklch(0.29 0.025 275);
--color-tm16-hi: #f2f2fb;
--color-tm16-hi: oklch(0.96 0.008 275);
--color-tm16-ink: #dcdcec;
--color-tm16-ink: oklch(0.89 0.015 275);
--color-tm16-mut: #9c9cba;
--color-tm16-mut: oklch(0.71 0.025 275);
--color-tm16-faint: #7c7c9a;
--color-tm16-faint: oklch(0.6 0.03 275);
--color-tm16-acc: #8aa0ff;
--color-tm16-acc: oklch(0.73 0.15 272);
--color-tm16-onacc: #070a1f;
--color-tm16-onacc: oklch(0.17 0.05 272);
--color-tm16-ring: #8aa0ff3d;
--color-tm16-ring: oklch(0.73 0.15 272 / 0.24);
--color-tm16-bad: #f08a8a;
--color-tm16-bad: oklch(0.72 0.15 22);
--color-tm16-ok: #7ae2a8;
--color-tm16-ok: oklch(0.85 0.16 155);
--color-tm16-oktint: #142a1f;
--color-tm16-oktint: oklch(0.25 0.05 155);
--color-tm16-okedge: #37785a;
--color-tm16-okedge: oklch(0.48 0.09 155);
--color-tm16-scrim: #08080da3;
--color-tm16-scrim: oklch(0.12 0.01 275 / 0.64);
--font-tm16-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-16 {
width: 100%;
}
#tm-16-dlg::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-16-dlg[open]::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-16');
var dlg = root.querySelector('#tm-16-dlg');
var form = root.querySelector('[data-tm16-form]');
var msg = root.querySelector('[data-tm16-msg]');
var count = root.querySelector('[data-tm16-count]');
root.querySelector('[data-tm16-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm16-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-cancel]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-done]').addEventListener('click', function () { dlg.close(); });
msg.addEventListener('input', function () { count.textContent = msg.value.length; });
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!form.checkValidity()) { form.reportValidity(); return; } // native constraints first
dlg.dataset.state = 'sent'; // values are never cleared
root.querySelector('[data-tm16-done]').focus();
});
dlg.addEventListener('close', function () { dlg.dataset.state = 'form'; });
})();
```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 Contact Support Modal with Live Validation
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-contact-support-modal-live-validation/
A support form that opens over the dashboard instead of navigating away: two-column identity fields, a topic select, priority radios and a counted message box. Validation styling uses user-invalid:, so a field only turns red after the user has actually left it wrong.
## HTML
```html
<section class="tm-16 w-full min-h-svh grid place-items-center bg-tm16-bg font-tm16-sans px-6 py-10">
<button type="button" data-tm16-open class="inline-flex items-center justify-center min-h-11 px-6 rounded-xl text-[15px] font-semibold text-tm16-ink bg-tm16-btn border border-tm16-btnedge cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover hover:border-tm16-acc focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">Contact support</button>
<dialog id="tm-16-dlg" data-state="form" aria-labelledby="tm-16-title" class="group m-auto w-[min(520px,94vw)] max-h-[92dvh] overflow-auto p-0 border-0 rounded-[20px] text-tm16-ink bg-tm16-card shadow-[0_40px_90px_-30px_#000,0_0_0_1px_var(--color-tm16-edge)] opacity-0 translate-y-3 scale-[0.97] transition-[opacity,translate,scale,overlay,display] duration-[280ms] transition-discrete open:opacity-100 open:translate-y-0 open:scale-100 open:starting:opacity-0 open:starting:translate-y-3 open:starting:scale-[0.97] open:backdrop:bg-tm16-scrim open:backdrop:backdrop-blur-[5px] motion-reduce:transition-none">
<div class="p-7">
<header class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 id="tm-16-title" class="text-[20px] font-bold text-tm16-hi">Contact support</h2>
<p class="mt-1 text-[13px] text-tm16-mut">We usually reply within a few hours.</p>
</div>
<button type="button" data-tm16-close aria-label="Close" class="grid place-items-center size-11 shrink-0 rounded-lg border-0 bg-transparent text-tm16-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm16-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2">×</button>
</header>
<form data-tm16-form novalidate class="flex flex-col gap-4 group-data-[state=sent]:hidden">
<div class="grid grid-cols-2 gap-3 max-[420px]:grid-cols-1">
<div>
<label for="tm-16-name" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Name</label>
<input id="tm-16-name" type="text" required autocomplete="name" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Please enter your name.</p>
</div>
<div>
<label for="tm-16-email" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Email</label>
<input id="tm-16-email" type="email" required autocomplete="email" class="peer w-full min-h-11 px-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<p class="hidden mt-1 text-[11.5px] text-tm16-bad peer-user-invalid:block">Enter a valid email address.</p>
</div>
</div>
<div>
<label for="tm-16-topic" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Topic</label>
<select id="tm-16-topic" required class="w-full min-h-11 px-3 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm text-tm16-hi cursor-pointer transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad">
<option value="">Choose a topic…</option>
<option>Billing & plans</option>
<option>Technical issue</option>
<option>Feature request</option>
<option>Something else</option>
</select>
</div>
<fieldset class="border-0 p-0">
<legend class="mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Priority</legend>
<div class="flex flex-wrap gap-4">
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" checked class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Low</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Normal</label>
<label class="inline-flex items-center gap-2 min-h-11 text-sm text-tm16-ink cursor-pointer"><input type="radio" name="tm-16-p" class="size-[18px] accent-tm16-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm16-acc focus-visible:outline-offset-2"> Urgent</label>
</div>
</fieldset>
<div>
<label for="tm-16-msg" class="block mb-1.5 text-[12.5px] font-semibold text-tm16-mut">Message</label>
<textarea id="tm-16-msg" data-tm16-msg required maxlength="500" rows="4" placeholder="Describe what's happening…" class="peer w-full min-h-28 max-h-60 p-3.5 rounded-[10px] border border-tm16-edge bg-tm16-well text-sm leading-relaxed text-tm16-hi placeholder:text-tm16-faint resize-y [field-sizing:content] transition-[border-color,box-shadow] duration-200 focus:outline-none focus:border-tm16-acc focus:shadow-[0_0_0_3px_var(--color-tm16-ring)] user-invalid:border-tm16-bad"></textarea>
<div class="flex justify-between mt-1">
<p class="hidden text-[11.5px] text-tm16-bad peer-user-invalid:block">Tell us what's happening.</p>
<p class="ml-auto text-[11.5px] text-tm16-faint"><b data-tm16-count class="text-tm16-mut">0</b>/500</p>
</div>
</div>
<div class="grid grid-cols-[auto_1fr] gap-3 mt-1">
<button type="button" data-tm16-cancel class="min-h-12 px-5 rounded-xl border border-tm16-btnedge bg-tm16-btn text-sm font-semibold text-tm16-ink cursor-pointer transition-colors duration-200 hover:bg-tm16-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Cancel</button>
<button type="submit" class="min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter,translate] duration-200 hover:brightness-110 active:translate-y-px focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 motion-reduce:active:translate-y-0">Send message</button>
</div>
</form>
<div role="status" class="hidden text-center group-data-[state=sent]:block">
<div aria-hidden="true" class="grid place-items-center size-14 mx-auto mb-4 rounded-2xl text-2xl bg-tm16-oktint text-tm16-ok shadow-[inset_0_0_0_1px_var(--color-tm16-okedge)]">✓</div>
<h3 class="mb-2 text-lg font-bold text-tm16-hi">Message sent</h3>
<p class="mb-5 text-sm leading-relaxed text-tm16-mut">Thanks — we've logged your request and emailed a copy to your inbox.</p>
<button type="button" data-tm16-done class="w-full min-h-12 rounded-xl border-0 bg-tm16-acc text-[14.5px] font-bold text-tm16-onacc cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Close</button>
</div>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm16-bg: #08080d;
--color-tm16-bg: oklch(0.14 0.012 275);
--color-tm16-card: #15161f;
--color-tm16-card: oklch(0.21 0.015 275);
--color-tm16-well: #0e0f18;
--color-tm16-well: oklch(0.17 0.013 275);
--color-tm16-btn: #1a1c2a;
--color-tm16-btn: oklch(0.23 0.02 275);
--color-tm16-btnhover: #232537;
--color-tm16-btnhover: oklch(0.27 0.028 275);
--color-tm16-btnedge: #333552;
--color-tm16-btnedge: oklch(0.32 0.03 275);
--color-tm16-edge: #2a2c42;
--color-tm16-edge: oklch(0.29 0.025 275);
--color-tm16-hi: #f2f2fb;
--color-tm16-hi: oklch(0.96 0.008 275);
--color-tm16-ink: #dcdcec;
--color-tm16-ink: oklch(0.89 0.015 275);
--color-tm16-mut: #9c9cba;
--color-tm16-mut: oklch(0.71 0.025 275);
--color-tm16-faint: #7c7c9a;
--color-tm16-faint: oklch(0.6 0.03 275);
--color-tm16-acc: #8aa0ff;
--color-tm16-acc: oklch(0.73 0.15 272);
--color-tm16-onacc: #070a1f;
--color-tm16-onacc: oklch(0.17 0.05 272);
--color-tm16-ring: #8aa0ff3d;
--color-tm16-ring: oklch(0.73 0.15 272 / 0.24);
--color-tm16-bad: #f08a8a;
--color-tm16-bad: oklch(0.72 0.15 22);
--color-tm16-ok: #7ae2a8;
--color-tm16-ok: oklch(0.85 0.16 155);
--color-tm16-oktint: #142a1f;
--color-tm16-oktint: oklch(0.25 0.05 155);
--color-tm16-okedge: #37785a;
--color-tm16-okedge: oklch(0.48 0.09 155);
--color-tm16-scrim: #08080da3;
--color-tm16-scrim: oklch(0.12 0.01 275 / 0.64);
--font-tm16-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-16 {
width: 100%;
}
#tm-16-dlg::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
transition: background .3s, backdrop-filter .3s, overlay .3s allow-discrete, display .3s allow-discrete;
}
@starting-style {
#tm-16-dlg[open]::backdrop {
background: oklch(0.12 0.01 275 / 0);
backdrop-filter: blur(0px);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-16');
var dlg = root.querySelector('#tm-16-dlg');
var form = root.querySelector('[data-tm16-form]');
var msg = root.querySelector('[data-tm16-msg]');
var count = root.querySelector('[data-tm16-count]');
root.querySelector('[data-tm16-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm16-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-cancel]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm16-done]').addEventListener('click', function () { dlg.close(); });
msg.addEventListener('input', function () { count.textContent = msg.value.length; });
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!form.checkValidity()) { form.reportValidity(); return; } // native constraints first
dlg.dataset.state = 'sent'; // values are never cleared
root.querySelector('[data-tm16-done]').focus();
});
dlg.addEventListener('close', function () { dlg.dataset.state = 'form'; });
})();
```How this works
:user-invalid is the whole validation story. :invalid matches a required field the moment the page loads, which is why hand-rolled form styling so often shows red before anyone has typed. :user-invalid waits for interaction and blur. One variant, and the form stops shouting at people who have done nothing wrong.
Native constraints first. required, type="email" and maxlength do the checking; the submit handler only runs checkValidity() and then swaps state. The inline hints are supplementary, not a reimplementation.
The confirmation is in the same dialog. data-state="sent" reveals a role="status" panel and focus moves to its close button — the user never leaves the context they opened the form from, which was the entire point of using a modal.
Make it yours
- Prefill name and email for signed-in users and hide the row; the grid collapses cleanly.
- Route by the topic
<select>value to the right queue on submit. - Add an attachment field —
file:mr-3 file:rounded-lg file:border-0 file:bg-tm16-btnstyles the native picker button.
Gotchas — read before shipping
- Never clear the form on a failed submit. Preserve every value; a wiped form after a network error is the fastest way to lose a support ticket and a customer.
- Let native validation fire before your handler. Reimplementing required-field checks in JS means two sources of truth and one of them will drift.
maxlengthsilently truncates paste. Pair it with the visible counter, as here, so the limit is discoverable before the user hits it.[field-sizing:content]is Chromium-only today; themin-h-28alongside it is what every other browser uses, so keep both.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ · 123+ field-sizing | 16.4+ | 128+ | 111+ · 123+ field-sizing |
:user-invalid is Baseline 2023; field-sizing is Chromium-only and purely an enhancement over the fixed min-h-28.