15 / 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 Filter & Sort Drawer that Becomes a Bottom Sheet
One dialog, two shapes: a right-docked full-height panel on desktop, a rounded bottom sheet under 520px — same markup, same transition contract, different axis. The responsive half of the port is max-[520px]: stacked in front of the open: and open:starting: variants.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tm-15 w-full min-h-svh grid place-items-center bg-tm15-bg font-tm15-sans px-6 py-10">
<button type="button" data-tm15-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-sm font-semibold text-tm15-ink bg-tm15-btn border border-tm15-edge cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover hover:border-tm15-acc focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"><span aria-hidden="true">⚙</span> Filter & sort</button>
<dialog id="tm-15-dlg" aria-labelledby="tm-15-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(380px,100vw)] p-0 border-0 rounded-none text-tm15-ink bg-tm15-card shadow-[-30px_0_80px_-20px_#000] translate-x-full transition-[translate,overlay,display] duration-[340ms] ease-[cubic-bezier(0.4,0,0.2,1)] transition-discrete open:translate-x-0 open:starting:translate-x-full open:backdrop:bg-tm15-scrim max-[520px]:mt-auto max-[520px]:mb-0 max-[520px]:mx-auto max-[520px]:h-auto max-[520px]:max-h-[86dvh] max-[520px]:w-screen max-[520px]:rounded-t-[20px] max-[520px]:translate-x-0 max-[520px]:translate-y-full max-[520px]:open:translate-y-0 max-[520px]:open:starting:translate-y-full motion-reduce:transition-none">
<form method="dialog" data-tm15-card class="flex flex-col h-full max-[520px]:h-auto">
<header class="flex items-center justify-between px-5 py-4 border-b border-tm15-edge">
<h2 id="tm-15-title" class="text-[17px] font-bold text-tm15-hi">Filter & sort</h2>
<button type="button" data-tm15-close aria-label="Close panel" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm15-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm15-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="flex-1 overflow-auto px-5 py-2">
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Sort by</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Featured</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: low to high</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: high to low</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Newest</label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Category</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Outerwear <span class="ml-auto text-xs text-tm15-faint">(128)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Knitwear <span class="ml-auto text-xs text-tm15-faint">(64)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Accessories <span class="ml-auto text-xs text-tm15-faint">(42)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Footwear <span class="ml-auto text-xs text-tm15-faint">(31)</span></label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Max price — <output data-tm15-out class="font-bold normal-case tracking-normal text-tm15-acc">$220</output></legend>
<input type="range" data-tm15-price min="20" max="400" step="10" value="220" aria-label="Maximum price" class="w-full accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-4 focus-visible:rounded">
</fieldset>
<fieldset class="border-0 p-0 py-4">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Colour</legend>
<div class="flex flex-wrap gap-2.5">
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#3a4a3f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Forest" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#5a3f2f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Rust" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#2c3440] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Slate" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#c9b58a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Sand" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#7a2f3a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Wine" class="absolute inset-0 opacity-0 cursor-pointer"></label>
</div>
</fieldset>
</div>
<footer class="flex gap-3 px-5 py-4 border-t border-tm15-edge bg-tm15-foot">
<button type="button" data-tm15-clear class="min-h-12 px-5 rounded-xl border border-tm15-edge bg-tm15-btn text-sm font-semibold text-tm15-ink cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Clear all</button>
<button type="button" data-tm15-apply class="flex-1 min-h-12 rounded-xl border-0 bg-tm15-acc text-[14.5px] font-bold text-tm15-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">Show <span data-tm15-n aria-live="polite">170</span> results</button>
</footer>
</form>
</dialog>
</section><section class="tm-15 w-full min-h-svh grid place-items-center bg-tm15-bg font-tm15-sans px-6 py-10">
<button type="button" data-tm15-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-sm font-semibold text-tm15-ink bg-tm15-btn border border-tm15-edge cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover hover:border-tm15-acc focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"><span aria-hidden="true">⚙</span> Filter & sort</button>
<dialog id="tm-15-dlg" aria-labelledby="tm-15-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(380px,100vw)] p-0 border-0 rounded-none text-tm15-ink bg-tm15-card shadow-[-30px_0_80px_-20px_#000] translate-x-full transition-[translate,overlay,display] duration-[340ms] ease-[cubic-bezier(0.4,0,0.2,1)] transition-discrete open:translate-x-0 open:starting:translate-x-full open:backdrop:bg-tm15-scrim max-[520px]:mt-auto max-[520px]:mb-0 max-[520px]:mx-auto max-[520px]:h-auto max-[520px]:max-h-[86dvh] max-[520px]:w-screen max-[520px]:rounded-t-[20px] max-[520px]:translate-x-0 max-[520px]:translate-y-full max-[520px]:open:translate-y-0 max-[520px]:open:starting:translate-y-full motion-reduce:transition-none">
<form method="dialog" data-tm15-card class="flex flex-col h-full max-[520px]:h-auto">
<header class="flex items-center justify-between px-5 py-4 border-b border-tm15-edge">
<h2 id="tm-15-title" class="text-[17px] font-bold text-tm15-hi">Filter & sort</h2>
<button type="button" data-tm15-close aria-label="Close panel" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm15-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm15-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="flex-1 overflow-auto px-5 py-2">
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Sort by</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Featured</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: low to high</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: high to low</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Newest</label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Category</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Outerwear <span class="ml-auto text-xs text-tm15-faint">(128)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Knitwear <span class="ml-auto text-xs text-tm15-faint">(64)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Accessories <span class="ml-auto text-xs text-tm15-faint">(42)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Footwear <span class="ml-auto text-xs text-tm15-faint">(31)</span></label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Max price — <output data-tm15-out class="font-bold normal-case tracking-normal text-tm15-acc">$220</output></legend>
<input type="range" data-tm15-price min="20" max="400" step="10" value="220" aria-label="Maximum price" class="w-full accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-4 focus-visible:rounded">
</fieldset>
<fieldset class="border-0 p-0 py-4">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Colour</legend>
<div class="flex flex-wrap gap-2.5">
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#3a4a3f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Forest" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#5a3f2f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Rust" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#2c3440] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Slate" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#c9b58a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Sand" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#7a2f3a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Wine" class="absolute inset-0 opacity-0 cursor-pointer"></label>
</div>
</fieldset>
</div>
<footer class="flex gap-3 px-5 py-4 border-t border-tm15-edge bg-tm15-foot">
<button type="button" data-tm15-clear class="min-h-12 px-5 rounded-xl border border-tm15-edge bg-tm15-btn text-sm font-semibold text-tm15-ink cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Clear all</button>
<button type="button" data-tm15-apply class="flex-1 min-h-12 rounded-xl border-0 bg-tm15-acc text-[14.5px] font-bold text-tm15-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">Show <span data-tm15-n aria-live="polite">170</span> results</button>
</footer>
</form>
</dialog>
</section>@import "tailwindcss";
@theme {
--color-tm15-bg: #07090a;
--color-tm15-bg: oklch(0.15 0.01 200);
--color-tm15-card: #131a1d;
--color-tm15-card: oklch(0.21 0.015 200);
--color-tm15-foot: #0f1618;
--color-tm15-foot: oklch(0.18 0.012 200);
--color-tm15-well: #1a2529;
--color-tm15-well: oklch(0.24 0.018 200);
--color-tm15-btn: #131c1f;
--color-tm15-btn: oklch(0.21 0.015 200);
--color-tm15-btnhover: #1b2529;
--color-tm15-btnhover: oklch(0.25 0.02 200);
--color-tm15-edge: #2a373b;
--color-tm15-edge: oklch(0.31 0.018 200);
--color-tm15-edgesoft: #1c262a;
--color-tm15-edgesoft: oklch(0.24 0.015 200);
--color-tm15-hi: #f0f6f7;
--color-tm15-hi: oklch(0.96 0.006 200);
--color-tm15-ink: #d3dddf;
--color-tm15-ink: oklch(0.88 0.012 200);
--color-tm15-mut: #9aa9ad;
--color-tm15-mut: oklch(0.73 0.018 200);
--color-tm15-faint: #7c8b8f;
--color-tm15-faint: oklch(0.62 0.018 200);
--color-tm15-acc: #4cc7d6;
--color-tm15-acc: oklch(0.77 0.12 200);
--color-tm15-onacc: #04161a;
--color-tm15-onacc: oklch(0.18 0.03 200);
--color-tm15-scrim: #0407088c;
--color-tm15-scrim: oklch(0.12 0.008 200 / 0.55);
--font-tm15-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-15 {
width: 100%;
}
#tm-15-dlg::backdrop {
background: oklch(0.12 0.008 200 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-15-dlg[open]::backdrop {
background: oklch(0.12 0.008 200 / 0);
}
}@import "tailwindcss";
@theme {
--color-tm15-bg: #07090a;
--color-tm15-bg: oklch(0.15 0.01 200);
--color-tm15-card: #131a1d;
--color-tm15-card: oklch(0.21 0.015 200);
--color-tm15-foot: #0f1618;
--color-tm15-foot: oklch(0.18 0.012 200);
--color-tm15-well: #1a2529;
--color-tm15-well: oklch(0.24 0.018 200);
--color-tm15-btn: #131c1f;
--color-tm15-btn: oklch(0.21 0.015 200);
--color-tm15-btnhover: #1b2529;
--color-tm15-btnhover: oklch(0.25 0.02 200);
--color-tm15-edge: #2a373b;
--color-tm15-edge: oklch(0.31 0.018 200);
--color-tm15-edgesoft: #1c262a;
--color-tm15-edgesoft: oklch(0.24 0.015 200);
--color-tm15-hi: #f0f6f7;
--color-tm15-hi: oklch(0.96 0.006 200);
--color-tm15-ink: #d3dddf;
--color-tm15-ink: oklch(0.88 0.012 200);
--color-tm15-mut: #9aa9ad;
--color-tm15-mut: oklch(0.73 0.018 200);
--color-tm15-faint: #7c8b8f;
--color-tm15-faint: oklch(0.62 0.018 200);
--color-tm15-acc: #4cc7d6;
--color-tm15-acc: oklch(0.77 0.12 200);
--color-tm15-onacc: #04161a;
--color-tm15-onacc: oklch(0.18 0.03 200);
--color-tm15-scrim: #0407088c;
--color-tm15-scrim: oklch(0.12 0.008 200 / 0.55);
--font-tm15-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-15 {
width: 100%;
}
#tm-15-dlg::backdrop {
background: oklch(0.12 0.008 200 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-15-dlg[open]::backdrop {
background: oklch(0.12 0.008 200 / 0);
}
}(function () {
var root = document.querySelector('.tm-15');
var dlg = root.querySelector('#tm-15-dlg');
var out = root.querySelector('[data-tm15-out]');
var price = root.querySelector('[data-tm15-price]');
var n = root.querySelector('[data-tm15-n]');
function recount() { // toy live count so the demo feels real
var cats = root.querySelectorAll('[data-tm15-cat]:checked').length;
n.textContent = Math.max(0, cats * 42 + Math.round((price.value - 20) / 380 * 40));
}
root.querySelector('[data-tm15-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm15-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm15-apply]').addEventListener('click', function () { dlg.close(); });
price.addEventListener('input', function () { out.textContent = '$' + price.value; recount(); });
root.querySelectorAll('[data-tm15-cat]').forEach(function (c) { c.addEventListener('change', recount); });
root.querySelector('[data-tm15-clear]').addEventListener('click', function () {
root.querySelectorAll('input[type=checkbox]').forEach(function (c) { c.checked = false; });
root.querySelector('input[type=radio]').checked = true;
price.value = 220; out.textContent = '$220'; recount();
});
recount();
})();(function () {
var root = document.querySelector('.tm-15');
var dlg = root.querySelector('#tm-15-dlg');
var out = root.querySelector('[data-tm15-out]');
var price = root.querySelector('[data-tm15-price]');
var n = root.querySelector('[data-tm15-n]');
function recount() { // toy live count so the demo feels real
var cats = root.querySelectorAll('[data-tm15-cat]:checked').length;
n.textContent = Math.max(0, cats * 42 + Math.round((price.value - 20) / 380 * 40));
}
root.querySelector('[data-tm15-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm15-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm15-apply]').addEventListener('click', function () { dlg.close(); });
price.addEventListener('input', function () { out.textContent = '$' + price.value; recount(); });
root.querySelectorAll('[data-tm15-cat]').forEach(function (c) { c.addEventListener('change', recount); });
root.querySelector('[data-tm15-clear]').addEventListener('click', function () {
root.querySelectorAll('input[type=checkbox]').forEach(function (c) { c.checked = false; });
root.querySelector('input[type=radio]').checked = true;
price.value = 220; out.textContent = '$220'; recount();
});
recount();
})();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-tm15-bg: #07090a;
--color-tm15-bg: oklch(0.15 0.01 200);
--color-tm15-card: #131a1d;
--color-tm15-card: oklch(0.21 0.015 200);
--color-tm15-foot: #0f1618;
--color-tm15-foot: oklch(0.18 0.012 200);
--color-tm15-well: #1a2529;
--color-tm15-well: oklch(0.24 0.018 200);
--color-tm15-btn: #131c1f;
--color-tm15-btn: oklch(0.21 0.015 200);
--color-tm15-btnhover: #1b2529;
--color-tm15-btnhover: oklch(0.25 0.02 200);
--color-tm15-edge: #2a373b;
--color-tm15-edge: oklch(0.31 0.018 200);
--color-tm15-edgesoft: #1c262a;
--color-tm15-edgesoft: oklch(0.24 0.015 200);
--color-tm15-hi: #f0f6f7;
--color-tm15-hi: oklch(0.96 0.006 200);
--color-tm15-ink: #d3dddf;
--color-tm15-ink: oklch(0.88 0.012 200);
--color-tm15-mut: #9aa9ad;
--color-tm15-mut: oklch(0.73 0.018 200);
--color-tm15-faint: #7c8b8f;
--color-tm15-faint: oklch(0.62 0.018 200);
--color-tm15-acc: #4cc7d6;
--color-tm15-acc: oklch(0.77 0.12 200);
--color-tm15-onacc: #04161a;
--color-tm15-onacc: oklch(0.18 0.03 200);
--color-tm15-scrim: #0407088c;
--color-tm15-scrim: oklch(0.12 0.008 200 / 0.55);
--font-tm15-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-15 {
width: 100%;
}
#tm-15-dlg::backdrop {
background: oklch(0.12 0.008 200 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-15-dlg[open]::backdrop {
background: oklch(0.12 0.008 200 / 0);
}
}@import "tailwindcss";
@theme {
--color-tm15-bg: #07090a;
--color-tm15-bg: oklch(0.15 0.01 200);
--color-tm15-card: #131a1d;
--color-tm15-card: oklch(0.21 0.015 200);
--color-tm15-foot: #0f1618;
--color-tm15-foot: oklch(0.18 0.012 200);
--color-tm15-well: #1a2529;
--color-tm15-well: oklch(0.24 0.018 200);
--color-tm15-btn: #131c1f;
--color-tm15-btn: oklch(0.21 0.015 200);
--color-tm15-btnhover: #1b2529;
--color-tm15-btnhover: oklch(0.25 0.02 200);
--color-tm15-edge: #2a373b;
--color-tm15-edge: oklch(0.31 0.018 200);
--color-tm15-edgesoft: #1c262a;
--color-tm15-edgesoft: oklch(0.24 0.015 200);
--color-tm15-hi: #f0f6f7;
--color-tm15-hi: oklch(0.96 0.006 200);
--color-tm15-ink: #d3dddf;
--color-tm15-ink: oklch(0.88 0.012 200);
--color-tm15-mut: #9aa9ad;
--color-tm15-mut: oklch(0.73 0.018 200);
--color-tm15-faint: #7c8b8f;
--color-tm15-faint: oklch(0.62 0.018 200);
--color-tm15-acc: #4cc7d6;
--color-tm15-acc: oklch(0.77 0.12 200);
--color-tm15-onacc: #04161a;
--color-tm15-onacc: oklch(0.18 0.03 200);
--color-tm15-scrim: #0407088c;
--color-tm15-scrim: oklch(0.12 0.008 200 / 0.55);
--font-tm15-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-15 {
width: 100%;
}
#tm-15-dlg::backdrop {
background: oklch(0.12 0.008 200 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-15-dlg[open]::backdrop {
background: oklch(0.12 0.008 200 / 0);
}
}<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 media query that reshaped the dialog becomes a run of
max-[520px]:utilities on the element — including the nested@starting-style, which in CSS had to be written as a media-query-wrapped at-rule and is here just another variant in the stack. accent-coloris one utility (accent-tm15-acc) and it replaces every custom radio, checkbox and slider-thumb rule a filter panel usually accumulates.- UI change from the CSS original: every filter row is a 44px target (the originals were 8px-padded labels, cramped for touch in exactly the component most used on mobile), swatches went 34px → 44px, and the result count is an
aria-liveregion so the number is announced as filters change.
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 Filter & Sort Drawer that Becomes a Bottom Sheet
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-filter-sort-drawer-bottom-sheet/
One dialog, two shapes: a right-docked full-height panel on desktop, a rounded bottom sheet under 520px — same markup, same transition contract, different axis. The responsive half of the port is max-[520px]: stacked in front of the open: and open:starting: variants.
## HTML
```html
<section class="tm-15 w-full min-h-svh grid place-items-center bg-tm15-bg font-tm15-sans px-6 py-10">
<button type="button" data-tm15-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-sm font-semibold text-tm15-ink bg-tm15-btn border border-tm15-edge cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover hover:border-tm15-acc focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"><span aria-hidden="true">⚙</span> Filter & sort</button>
<dialog id="tm-15-dlg" aria-labelledby="tm-15-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(380px,100vw)] p-0 border-0 rounded-none text-tm15-ink bg-tm15-card shadow-[-30px_0_80px_-20px_#000] translate-x-full transition-[translate,overlay,display] duration-[340ms] ease-[cubic-bezier(0.4,0,0.2,1)] transition-discrete open:translate-x-0 open:starting:translate-x-full open:backdrop:bg-tm15-scrim max-[520px]:mt-auto max-[520px]:mb-0 max-[520px]:mx-auto max-[520px]:h-auto max-[520px]:max-h-[86dvh] max-[520px]:w-screen max-[520px]:rounded-t-[20px] max-[520px]:translate-x-0 max-[520px]:translate-y-full max-[520px]:open:translate-y-0 max-[520px]:open:starting:translate-y-full motion-reduce:transition-none">
<form method="dialog" data-tm15-card class="flex flex-col h-full max-[520px]:h-auto">
<header class="flex items-center justify-between px-5 py-4 border-b border-tm15-edge">
<h2 id="tm-15-title" class="text-[17px] font-bold text-tm15-hi">Filter & sort</h2>
<button type="button" data-tm15-close aria-label="Close panel" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm15-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm15-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="flex-1 overflow-auto px-5 py-2">
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Sort by</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Featured</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: low to high</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: high to low</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Newest</label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Category</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Outerwear <span class="ml-auto text-xs text-tm15-faint">(128)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Knitwear <span class="ml-auto text-xs text-tm15-faint">(64)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Accessories <span class="ml-auto text-xs text-tm15-faint">(42)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Footwear <span class="ml-auto text-xs text-tm15-faint">(31)</span></label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Max price — <output data-tm15-out class="font-bold normal-case tracking-normal text-tm15-acc">$220</output></legend>
<input type="range" data-tm15-price min="20" max="400" step="10" value="220" aria-label="Maximum price" class="w-full accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-4 focus-visible:rounded">
</fieldset>
<fieldset class="border-0 p-0 py-4">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Colour</legend>
<div class="flex flex-wrap gap-2.5">
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#3a4a3f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Forest" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#5a3f2f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Rust" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#2c3440] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Slate" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#c9b58a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Sand" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#7a2f3a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Wine" class="absolute inset-0 opacity-0 cursor-pointer"></label>
</div>
</fieldset>
</div>
<footer class="flex gap-3 px-5 py-4 border-t border-tm15-edge bg-tm15-foot">
<button type="button" data-tm15-clear class="min-h-12 px-5 rounded-xl border border-tm15-edge bg-tm15-btn text-sm font-semibold text-tm15-ink cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Clear all</button>
<button type="button" data-tm15-apply class="flex-1 min-h-12 rounded-xl border-0 bg-tm15-acc text-[14.5px] font-bold text-tm15-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">Show <span data-tm15-n aria-live="polite">170</span> results</button>
</footer>
</form>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm15-bg: #07090a;
--color-tm15-bg: oklch(0.15 0.01 200);
--color-tm15-card: #131a1d;
--color-tm15-card: oklch(0.21 0.015 200);
--color-tm15-foot: #0f1618;
--color-tm15-foot: oklch(0.18 0.012 200);
--color-tm15-well: #1a2529;
--color-tm15-well: oklch(0.24 0.018 200);
--color-tm15-btn: #131c1f;
--color-tm15-btn: oklch(0.21 0.015 200);
--color-tm15-btnhover: #1b2529;
--color-tm15-btnhover: oklch(0.25 0.02 200);
--color-tm15-edge: #2a373b;
--color-tm15-edge: oklch(0.31 0.018 200);
--color-tm15-edgesoft: #1c262a;
--color-tm15-edgesoft: oklch(0.24 0.015 200);
--color-tm15-hi: #f0f6f7;
--color-tm15-hi: oklch(0.96 0.006 200);
--color-tm15-ink: #d3dddf;
--color-tm15-ink: oklch(0.88 0.012 200);
--color-tm15-mut: #9aa9ad;
--color-tm15-mut: oklch(0.73 0.018 200);
--color-tm15-faint: #7c8b8f;
--color-tm15-faint: oklch(0.62 0.018 200);
--color-tm15-acc: #4cc7d6;
--color-tm15-acc: oklch(0.77 0.12 200);
--color-tm15-onacc: #04161a;
--color-tm15-onacc: oklch(0.18 0.03 200);
--color-tm15-scrim: #0407088c;
--color-tm15-scrim: oklch(0.12 0.008 200 / 0.55);
--font-tm15-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-15 {
width: 100%;
}
#tm-15-dlg::backdrop {
background: oklch(0.12 0.008 200 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-15-dlg[open]::backdrop {
background: oklch(0.12 0.008 200 / 0);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-15');
var dlg = root.querySelector('#tm-15-dlg');
var out = root.querySelector('[data-tm15-out]');
var price = root.querySelector('[data-tm15-price]');
var n = root.querySelector('[data-tm15-n]');
function recount() { // toy live count so the demo feels real
var cats = root.querySelectorAll('[data-tm15-cat]:checked').length;
n.textContent = Math.max(0, cats * 42 + Math.round((price.value - 20) / 380 * 40));
}
root.querySelector('[data-tm15-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm15-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm15-apply]').addEventListener('click', function () { dlg.close(); });
price.addEventListener('input', function () { out.textContent = '$' + price.value; recount(); });
root.querySelectorAll('[data-tm15-cat]').forEach(function (c) { c.addEventListener('change', recount); });
root.querySelector('[data-tm15-clear]').addEventListener('click', function () {
root.querySelectorAll('input[type=checkbox]').forEach(function (c) { c.checked = false; });
root.querySelector('input[type=radio]').checked = true;
price.value = 220; out.textContent = '$220'; recount();
});
recount();
})();
```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 Filter & Sort Drawer that Becomes a Bottom Sheet
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-filter-sort-drawer-bottom-sheet/
One dialog, two shapes: a right-docked full-height panel on desktop, a rounded bottom sheet under 520px — same markup, same transition contract, different axis. The responsive half of the port is max-[520px]: stacked in front of the open: and open:starting: variants.
## HTML
```html
<section class="tm-15 w-full min-h-svh grid place-items-center bg-tm15-bg font-tm15-sans px-6 py-10">
<button type="button" data-tm15-open class="inline-flex items-center gap-2.5 min-h-11 px-6 rounded-xl text-sm font-semibold text-tm15-ink bg-tm15-btn border border-tm15-edge cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover hover:border-tm15-acc focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"><span aria-hidden="true">⚙</span> Filter & sort</button>
<dialog id="tm-15-dlg" aria-labelledby="tm-15-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(380px,100vw)] p-0 border-0 rounded-none text-tm15-ink bg-tm15-card shadow-[-30px_0_80px_-20px_#000] translate-x-full transition-[translate,overlay,display] duration-[340ms] ease-[cubic-bezier(0.4,0,0.2,1)] transition-discrete open:translate-x-0 open:starting:translate-x-full open:backdrop:bg-tm15-scrim max-[520px]:mt-auto max-[520px]:mb-0 max-[520px]:mx-auto max-[520px]:h-auto max-[520px]:max-h-[86dvh] max-[520px]:w-screen max-[520px]:rounded-t-[20px] max-[520px]:translate-x-0 max-[520px]:translate-y-full max-[520px]:open:translate-y-0 max-[520px]:open:starting:translate-y-full motion-reduce:transition-none">
<form method="dialog" data-tm15-card class="flex flex-col h-full max-[520px]:h-auto">
<header class="flex items-center justify-between px-5 py-4 border-b border-tm15-edge">
<h2 id="tm-15-title" class="text-[17px] font-bold text-tm15-hi">Filter & sort</h2>
<button type="button" data-tm15-close aria-label="Close panel" class="grid place-items-center size-11 rounded-lg border-0 bg-transparent text-tm15-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm15-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="flex-1 overflow-auto px-5 py-2">
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Sort by</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Featured</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: low to high</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Price: high to low</label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="radio" name="tm-15-sort" class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Newest</label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Category</legend>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Outerwear <span class="ml-auto text-xs text-tm15-faint">(128)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Knitwear <span class="ml-auto text-xs text-tm15-faint">(64)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat checked class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Accessories <span class="ml-auto text-xs text-tm15-faint">(42)</span></label>
<label class="flex items-center gap-3 min-h-11 text-sm text-tm15-ink cursor-pointer"><input type="checkbox" data-tm15-cat class="size-[18px] accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-2"> Footwear <span class="ml-auto text-xs text-tm15-faint">(31)</span></label>
</fieldset>
<fieldset class="border-0 p-0 py-4 border-b border-tm15-edgesoft">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Max price — <output data-tm15-out class="font-bold normal-case tracking-normal text-tm15-acc">$220</output></legend>
<input type="range" data-tm15-price min="20" max="400" step="10" value="220" aria-label="Maximum price" class="w-full accent-tm15-acc cursor-pointer focus-visible:outline-2 focus-visible:outline-tm15-acc focus-visible:outline-offset-4 focus-visible:rounded">
</fieldset>
<fieldset class="border-0 p-0 py-4">
<legend class="mb-3 text-xs uppercase tracking-[0.14em] text-tm15-mut">Colour</legend>
<div class="flex flex-wrap gap-2.5">
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#3a4a3f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Forest" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#5a3f2f] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Rust" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#2c3440] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Slate" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#c9b58a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Sand" class="absolute inset-0 opacity-0 cursor-pointer"></label>
<label class="relative size-11 rounded-[10px] cursor-pointer bg-[#7a2f3a] shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_1px_var(--color-tm15-edge)] has-checked:shadow-[inset_0_0_0_2px_var(--color-tm15-card),0_0_0_2px_var(--color-tm15-acc)] has-focus-visible:outline-2 has-focus-visible:outline-tm15-acc has-focus-visible:outline-offset-[3px]"><input type="checkbox" aria-label="Wine" class="absolute inset-0 opacity-0 cursor-pointer"></label>
</div>
</fieldset>
</div>
<footer class="flex gap-3 px-5 py-4 border-t border-tm15-edge bg-tm15-foot">
<button type="button" data-tm15-clear class="min-h-12 px-5 rounded-xl border border-tm15-edge bg-tm15-btn text-sm font-semibold text-tm15-ink cursor-pointer transition-colors duration-200 hover:bg-tm15-btnhover focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">Clear all</button>
<button type="button" data-tm15-apply class="flex-1 min-h-12 rounded-xl border-0 bg-tm15-acc text-[14.5px] font-bold text-tm15-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">Show <span data-tm15-n aria-live="polite">170</span> results</button>
</footer>
</form>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm15-bg: #07090a;
--color-tm15-bg: oklch(0.15 0.01 200);
--color-tm15-card: #131a1d;
--color-tm15-card: oklch(0.21 0.015 200);
--color-tm15-foot: #0f1618;
--color-tm15-foot: oklch(0.18 0.012 200);
--color-tm15-well: #1a2529;
--color-tm15-well: oklch(0.24 0.018 200);
--color-tm15-btn: #131c1f;
--color-tm15-btn: oklch(0.21 0.015 200);
--color-tm15-btnhover: #1b2529;
--color-tm15-btnhover: oklch(0.25 0.02 200);
--color-tm15-edge: #2a373b;
--color-tm15-edge: oklch(0.31 0.018 200);
--color-tm15-edgesoft: #1c262a;
--color-tm15-edgesoft: oklch(0.24 0.015 200);
--color-tm15-hi: #f0f6f7;
--color-tm15-hi: oklch(0.96 0.006 200);
--color-tm15-ink: #d3dddf;
--color-tm15-ink: oklch(0.88 0.012 200);
--color-tm15-mut: #9aa9ad;
--color-tm15-mut: oklch(0.73 0.018 200);
--color-tm15-faint: #7c8b8f;
--color-tm15-faint: oklch(0.62 0.018 200);
--color-tm15-acc: #4cc7d6;
--color-tm15-acc: oklch(0.77 0.12 200);
--color-tm15-onacc: #04161a;
--color-tm15-onacc: oklch(0.18 0.03 200);
--color-tm15-scrim: #0407088c;
--color-tm15-scrim: oklch(0.12 0.008 200 / 0.55);
--font-tm15-sans: 'Space Grotesk', system-ui, sans-serif;
}
.tm-15 {
width: 100%;
}
#tm-15-dlg::backdrop {
background: oklch(0.12 0.008 200 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-15-dlg[open]::backdrop {
background: oklch(0.12 0.008 200 / 0);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-15');
var dlg = root.querySelector('#tm-15-dlg');
var out = root.querySelector('[data-tm15-out]');
var price = root.querySelector('[data-tm15-price]');
var n = root.querySelector('[data-tm15-n]');
function recount() { // toy live count so the demo feels real
var cats = root.querySelectorAll('[data-tm15-cat]:checked').length;
n.textContent = Math.max(0, cats * 42 + Math.round((price.value - 20) / 380 * 40));
}
root.querySelector('[data-tm15-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm15-close]').addEventListener('click', function () { dlg.close(); });
root.querySelector('[data-tm15-apply]').addEventListener('click', function () { dlg.close(); });
price.addEventListener('input', function () { out.textContent = '$' + price.value; recount(); });
root.querySelectorAll('[data-tm15-cat]').forEach(function (c) { c.addEventListener('change', recount); });
root.querySelector('[data-tm15-clear]').addEventListener('click', function () {
root.querySelectorAll('input[type=checkbox]').forEach(function (c) { c.checked = false; });
root.querySelector('input[type=radio]').checked = true;
price.value = 220; out.textContent = '$220'; recount();
});
recount();
})();
```How this works
One element, two form factors. Desktop: ml-auto h-full and a translate on X. Mobile: mt-auto w-screen, rounded top corners and a translate on Y. Because both states are utilities on the same dialog, there is no second component to keep in sync — and the animation contract (transition-discrete plus open:starting:) is written once.
Native inputs, brand colour. accent-color is the single most under-used CSS property in filter UIs: real radios, checkboxes and range thumbs in the accent hue, with keyboard behaviour, form semantics and platform affordances intact. Custom-drawn controls buy styling and pay in accessibility.
The count is live. Every change recomputes an aria-live result figure in the Apply button, so no one applies a filter set into an empty grid.
Make it yours
- Change the breakpoint in one place:
max-[520px]:→max-sm:ormax-[680px]:. - Add facets by copying a
<fieldset>; the scroll area and pinned footer absorb any number of them. - Wire Apply to your query params and echo active filters as removable chips above the grid.
Gotchas — read before shipping
- Keep the footer outside the scroll area (
flex-1 overflow-autoon the middle, footer as a sibling) or Apply scrolls out of reach exactly when the user is ready to press it. - Cap the sheet at
max-h-[86dvh]. A sheet taller than the viewport cannot be dismissed by dragging on a phone and the top controls end up under the status bar. - Swatches must be real inputs with
aria-labels — a coloured square is not a label, and 'blue' is not a product colour name. - The mobile variant must restate
open:starting:on the Y axis. Forgetting it leaves the sheet animating on X inside a media query where it has no X offset, so it simply appears.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ · 117+ anim | 16.4+ · 17.5+ anim | 128+ · 129+ anim | 111+ · 117+ anim |
<dialog>, accent-color and :has() are Baseline; the slide needs @starting-style and degrades to an instant open on both axes.