06 / 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 Shopping Cart Slide-Over Drawer with Dialog
A right-edge mini-cart that slides in over an inert page: line items, a free-shipping progress bar, a live subtotal and a checkout CTA. The slide is translate-x-full → open:translate-x-0 with open:starting:translate-x-full — in and out, with no JS timers and no transition-end listeners.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tm-06 w-full min-h-svh grid place-items-center bg-tm06-bg font-tm06-sans px-6 py-10">
<button type="button" data-tm06-open class="inline-flex items-center gap-2.5 min-h-11 px-5 rounded-xl text-sm font-semibold text-tm06-ink bg-tm06-btn border border-tm06-edge cursor-pointer transition-colors duration-200 hover:bg-tm06-btnhover hover:border-tm06-acc focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">
<span aria-hidden="true">🛍</span> Your bag <span class="grid place-items-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-tm06-acc text-xs font-bold text-tm06-onacc">2</span>
</button>
<dialog id="tm-06-dlg" aria-labelledby="tm-06-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(400px,100vw)] p-0 border-0 rounded-none text-tm06-ink bg-tm06-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-tm06-scrim motion-reduce:transition-none">
<div data-tm06-card class="flex flex-col h-full">
<header class="flex items-center justify-between px-5 pt-5 pb-4 border-b border-tm06-edge">
<h2 id="tm-06-title" class="text-lg font-bold text-tm06-hi">Your bag</h2>
<button type="button" data-tm06-close aria-label="Close bag" class="grid place-items-center size-9 rounded-lg border-0 bg-transparent text-tm06-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm06-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="px-5 py-4 border-b border-tm06-edge">
<p class="mb-2.5 text-[13px] text-tm06-mut"><b data-tm06-left class="font-semibold text-tm06-acc">$52</b> away from free shipping</p>
<div data-tm06-bar role="progressbar" aria-valuemin="0" aria-valuemax="200" aria-valuenow="148" aria-label="Progress to free shipping" class="h-1.5 rounded-full bg-tm06-well overflow-hidden">
<i data-tm06-fill style="width:74%" class="block h-full rounded-full bg-tm06-acc transition-[width] duration-300 motion-reduce:transition-none"></i>
</div>
</div>
<ul class="flex-1 overflow-auto list-none m-0 px-5 py-2">
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Merino Overshirt</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Forest · M</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="98" class="text-sm font-bold text-tm06-hi">$98</p>
</li>
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Canvas Field Cap</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Rust · OS</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="50" class="text-sm font-bold text-tm06-hi">$50</p>
</li>
</ul>
<footer class="px-5 pt-4 pb-5 border-t border-tm06-edge bg-tm06-foot">
<div class="flex items-baseline justify-between mb-3.5">
<span class="text-sm text-tm06-mut">Subtotal</span>
<b data-tm06-total aria-live="polite" class="text-[22px] text-white">$148</b>
</div>
<button type="button" class="w-full min-h-12 rounded-xl border-0 bg-tm06-acc text-[15px] font-bold text-tm06-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">Checkout</button>
<p class="mt-3 text-center text-[11.5px] text-tm06-faint">Shipping & taxes calculated at checkout</p>
</footer>
</div>
</dialog>
</section><section class="tm-06 w-full min-h-svh grid place-items-center bg-tm06-bg font-tm06-sans px-6 py-10">
<button type="button" data-tm06-open class="inline-flex items-center gap-2.5 min-h-11 px-5 rounded-xl text-sm font-semibold text-tm06-ink bg-tm06-btn border border-tm06-edge cursor-pointer transition-colors duration-200 hover:bg-tm06-btnhover hover:border-tm06-acc focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">
<span aria-hidden="true">🛍</span> Your bag <span class="grid place-items-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-tm06-acc text-xs font-bold text-tm06-onacc">2</span>
</button>
<dialog id="tm-06-dlg" aria-labelledby="tm-06-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(400px,100vw)] p-0 border-0 rounded-none text-tm06-ink bg-tm06-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-tm06-scrim motion-reduce:transition-none">
<div data-tm06-card class="flex flex-col h-full">
<header class="flex items-center justify-between px-5 pt-5 pb-4 border-b border-tm06-edge">
<h2 id="tm-06-title" class="text-lg font-bold text-tm06-hi">Your bag</h2>
<button type="button" data-tm06-close aria-label="Close bag" class="grid place-items-center size-9 rounded-lg border-0 bg-transparent text-tm06-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm06-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="px-5 py-4 border-b border-tm06-edge">
<p class="mb-2.5 text-[13px] text-tm06-mut"><b data-tm06-left class="font-semibold text-tm06-acc">$52</b> away from free shipping</p>
<div data-tm06-bar role="progressbar" aria-valuemin="0" aria-valuemax="200" aria-valuenow="148" aria-label="Progress to free shipping" class="h-1.5 rounded-full bg-tm06-well overflow-hidden">
<i data-tm06-fill style="width:74%" class="block h-full rounded-full bg-tm06-acc transition-[width] duration-300 motion-reduce:transition-none"></i>
</div>
</div>
<ul class="flex-1 overflow-auto list-none m-0 px-5 py-2">
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Merino Overshirt</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Forest · M</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="98" class="text-sm font-bold text-tm06-hi">$98</p>
</li>
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Canvas Field Cap</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Rust · OS</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="50" class="text-sm font-bold text-tm06-hi">$50</p>
</li>
</ul>
<footer class="px-5 pt-4 pb-5 border-t border-tm06-edge bg-tm06-foot">
<div class="flex items-baseline justify-between mb-3.5">
<span class="text-sm text-tm06-mut">Subtotal</span>
<b data-tm06-total aria-live="polite" class="text-[22px] text-white">$148</b>
</div>
<button type="button" class="w-full min-h-12 rounded-xl border-0 bg-tm06-acc text-[15px] font-bold text-tm06-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">Checkout</button>
<p class="mt-3 text-center text-[11.5px] text-tm06-faint">Shipping & taxes calculated at checkout</p>
</footer>
</div>
</dialog>
</section>@import "tailwindcss";
@theme {
--color-tm06-bg: #0a0706;
--color-tm06-bg: oklch(0.15 0.015 30);
--color-tm06-card: #191210;
--color-tm06-card: oklch(0.21 0.018 30);
--color-tm06-foot: #140d0b;
--color-tm06-foot: oklch(0.18 0.015 30);
--color-tm06-well: #1a1310;
--color-tm06-well: oklch(0.2 0.018 30);
--color-tm06-btn: #1a1310;
--color-tm06-btn: oklch(0.2 0.018 30);
--color-tm06-btnhover: #241a16;
--color-tm06-btnhover: oklch(0.25 0.025 30);
--color-tm06-edge: #33251f;
--color-tm06-edge: oklch(0.3 0.025 30);
--color-tm06-stripeA: #241a15;
--color-tm06-stripeA: oklch(0.24 0.02 30);
--color-tm06-stripeB: #1c130f;
--color-tm06-stripeB: oklch(0.21 0.018 30);
--color-tm06-hi: #f7ede9;
--color-tm06-hi: oklch(0.95 0.012 30);
--color-tm06-ink: #e5d5cf;
--color-tm06-ink: oklch(0.89 0.018 30);
--color-tm06-mut: #b09a92;
--color-tm06-mut: oklch(0.73 0.025 30);
--color-tm06-faint: #8a746b;
--color-tm06-faint: oklch(0.6 0.025 30);
--color-tm06-acc: #f08a63;
--color-tm06-acc: oklch(0.74 0.15 40);
--color-tm06-onacc: #180a07;
--color-tm06-onacc: oklch(0.2 0.04 40);
--color-tm06-scrim: #0604038c;
--color-tm06-scrim: oklch(0.12 0.01 30 / 0.55);
--font-tm06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tm-06 {
width: 100%;
}
#tm-06-dlg::backdrop {
background: oklch(0.12 0.01 30 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-06-dlg[open]::backdrop {
background: oklch(0.12 0.01 30 / 0);
}
}@import "tailwindcss";
@theme {
--color-tm06-bg: #0a0706;
--color-tm06-bg: oklch(0.15 0.015 30);
--color-tm06-card: #191210;
--color-tm06-card: oklch(0.21 0.018 30);
--color-tm06-foot: #140d0b;
--color-tm06-foot: oklch(0.18 0.015 30);
--color-tm06-well: #1a1310;
--color-tm06-well: oklch(0.2 0.018 30);
--color-tm06-btn: #1a1310;
--color-tm06-btn: oklch(0.2 0.018 30);
--color-tm06-btnhover: #241a16;
--color-tm06-btnhover: oklch(0.25 0.025 30);
--color-tm06-edge: #33251f;
--color-tm06-edge: oklch(0.3 0.025 30);
--color-tm06-stripeA: #241a15;
--color-tm06-stripeA: oklch(0.24 0.02 30);
--color-tm06-stripeB: #1c130f;
--color-tm06-stripeB: oklch(0.21 0.018 30);
--color-tm06-hi: #f7ede9;
--color-tm06-hi: oklch(0.95 0.012 30);
--color-tm06-ink: #e5d5cf;
--color-tm06-ink: oklch(0.89 0.018 30);
--color-tm06-mut: #b09a92;
--color-tm06-mut: oklch(0.73 0.025 30);
--color-tm06-faint: #8a746b;
--color-tm06-faint: oklch(0.6 0.025 30);
--color-tm06-acc: #f08a63;
--color-tm06-acc: oklch(0.74 0.15 40);
--color-tm06-onacc: #180a07;
--color-tm06-onacc: oklch(0.2 0.04 40);
--color-tm06-scrim: #0604038c;
--color-tm06-scrim: oklch(0.12 0.01 30 / 0.55);
--font-tm06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tm-06 {
width: 100%;
}
#tm-06-dlg::backdrop {
background: oklch(0.12 0.01 30 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-06-dlg[open]::backdrop {
background: oklch(0.12 0.01 30 / 0);
}
}(function () {
var root = document.querySelector('.tm-06');
var dlg = root.querySelector('#tm-06-dlg');
var card = root.querySelector('[data-tm06-card]');
var THRESHOLD = 200;
root.querySelector('[data-tm06-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm06-close]').addEventListener('click', function () { dlg.close(); });
function recalc() {
var sum = 0;
root.querySelectorAll('[data-tm06-item]').forEach(function (li) {
var q = parseInt(li.querySelector('[data-tm06-qty]').textContent, 10);
var line = li.querySelector('[data-tm06-line]');
var base = parseInt(line.dataset.price, 10);
line.textContent = '$' + base * q;
sum += base * q;
});
root.querySelector('[data-tm06-total]').textContent = '$' + sum;
root.querySelector('[data-tm06-fill]').style.width = Math.min(100, sum / THRESHOLD * 100) + '%';
root.querySelector('[data-tm06-left]').textContent = '$' + Math.max(0, THRESHOLD - sum);
root.querySelector('[data-tm06-bar]').setAttribute('aria-valuenow', sum);
}
root.querySelectorAll('[data-tm06-step]').forEach(function (b) {
b.addEventListener('click', function () {
var span = b.parentNode.querySelector('[data-tm06-qty]');
span.textContent = Math.max(1, parseInt(span.textContent, 10) + parseInt(b.dataset.tm06Step, 10));
recalc();
});
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();(function () {
var root = document.querySelector('.tm-06');
var dlg = root.querySelector('#tm-06-dlg');
var card = root.querySelector('[data-tm06-card]');
var THRESHOLD = 200;
root.querySelector('[data-tm06-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm06-close]').addEventListener('click', function () { dlg.close(); });
function recalc() {
var sum = 0;
root.querySelectorAll('[data-tm06-item]').forEach(function (li) {
var q = parseInt(li.querySelector('[data-tm06-qty]').textContent, 10);
var line = li.querySelector('[data-tm06-line]');
var base = parseInt(line.dataset.price, 10);
line.textContent = '$' + base * q;
sum += base * q;
});
root.querySelector('[data-tm06-total]').textContent = '$' + sum;
root.querySelector('[data-tm06-fill]').style.width = Math.min(100, sum / THRESHOLD * 100) + '%';
root.querySelector('[data-tm06-left]').textContent = '$' + Math.max(0, THRESHOLD - sum);
root.querySelector('[data-tm06-bar]').setAttribute('aria-valuenow', sum);
}
root.querySelectorAll('[data-tm06-step]').forEach(function (b) {
b.addEventListener('click', function () {
var span = b.parentNode.querySelector('[data-tm06-qty]');
span.textContent = Math.max(1, parseInt(span.textContent, 10) + parseInt(b.dataset.tm06Step, 10));
recalc();
});
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tm06-bg: #0a0706;
--color-tm06-bg: oklch(0.15 0.015 30);
--color-tm06-card: #191210;
--color-tm06-card: oklch(0.21 0.018 30);
--color-tm06-foot: #140d0b;
--color-tm06-foot: oklch(0.18 0.015 30);
--color-tm06-well: #1a1310;
--color-tm06-well: oklch(0.2 0.018 30);
--color-tm06-btn: #1a1310;
--color-tm06-btn: oklch(0.2 0.018 30);
--color-tm06-btnhover: #241a16;
--color-tm06-btnhover: oklch(0.25 0.025 30);
--color-tm06-edge: #33251f;
--color-tm06-edge: oklch(0.3 0.025 30);
--color-tm06-stripeA: #241a15;
--color-tm06-stripeA: oklch(0.24 0.02 30);
--color-tm06-stripeB: #1c130f;
--color-tm06-stripeB: oklch(0.21 0.018 30);
--color-tm06-hi: #f7ede9;
--color-tm06-hi: oklch(0.95 0.012 30);
--color-tm06-ink: #e5d5cf;
--color-tm06-ink: oklch(0.89 0.018 30);
--color-tm06-mut: #b09a92;
--color-tm06-mut: oklch(0.73 0.025 30);
--color-tm06-faint: #8a746b;
--color-tm06-faint: oklch(0.6 0.025 30);
--color-tm06-acc: #f08a63;
--color-tm06-acc: oklch(0.74 0.15 40);
--color-tm06-onacc: #180a07;
--color-tm06-onacc: oklch(0.2 0.04 40);
--color-tm06-scrim: #0604038c;
--color-tm06-scrim: oklch(0.12 0.01 30 / 0.55);
--font-tm06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tm-06 {
width: 100%;
}
#tm-06-dlg::backdrop {
background: oklch(0.12 0.01 30 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-06-dlg[open]::backdrop {
background: oklch(0.12 0.01 30 / 0);
}
}@import "tailwindcss";
@theme {
--color-tm06-bg: #0a0706;
--color-tm06-bg: oklch(0.15 0.015 30);
--color-tm06-card: #191210;
--color-tm06-card: oklch(0.21 0.018 30);
--color-tm06-foot: #140d0b;
--color-tm06-foot: oklch(0.18 0.015 30);
--color-tm06-well: #1a1310;
--color-tm06-well: oklch(0.2 0.018 30);
--color-tm06-btn: #1a1310;
--color-tm06-btn: oklch(0.2 0.018 30);
--color-tm06-btnhover: #241a16;
--color-tm06-btnhover: oklch(0.25 0.025 30);
--color-tm06-edge: #33251f;
--color-tm06-edge: oklch(0.3 0.025 30);
--color-tm06-stripeA: #241a15;
--color-tm06-stripeA: oklch(0.24 0.02 30);
--color-tm06-stripeB: #1c130f;
--color-tm06-stripeB: oklch(0.21 0.018 30);
--color-tm06-hi: #f7ede9;
--color-tm06-hi: oklch(0.95 0.012 30);
--color-tm06-ink: #e5d5cf;
--color-tm06-ink: oklch(0.89 0.018 30);
--color-tm06-mut: #b09a92;
--color-tm06-mut: oklch(0.73 0.025 30);
--color-tm06-faint: #8a746b;
--color-tm06-faint: oklch(0.6 0.025 30);
--color-tm06-acc: #f08a63;
--color-tm06-acc: oklch(0.74 0.15 40);
--color-tm06-onacc: #180a07;
--color-tm06-onacc: oklch(0.2 0.04 40);
--color-tm06-scrim: #0604038c;
--color-tm06-scrim: oklch(0.12 0.01 30 / 0.55);
--font-tm06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tm-06 {
width: 100%;
}
#tm-06-dlg::backdrop {
background: oklch(0.12 0.01 30 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-06-dlg[open]::backdrop {
background: oklch(0.12 0.01 30 / 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&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The drawer's three-state slide (
translateX(100%),[open],@starting-style) is one class list. Reading it left to right tells you the whole animation, which the CSS version could not. ml-auto mr-0 my-0replacesmargin:0 0 0 auto. Slightly noisier, but the direction of the drawer is now greppable in the markup.- UI change from the CSS original: quantity steppers went from 30×30px to 44×44px (the original failed the touch floor in the place it matters most), the subtotal became an
aria-liveregion so it is announced on change, and the progress fill transitions its width instead of jumping.
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 Shopping Cart Slide-Over Drawer with Dialog
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-shopping-cart-slide-over-drawer-dialog/
A right-edge mini-cart that slides in over an inert page: line items, a free-shipping progress bar, a live subtotal and a checkout CTA. The slide is translate-x-full → open:translate-x-0 with open:starting:translate-x-full — in and out, with no JS timers and no transition-end listeners.
## HTML
```html
<section class="tm-06 w-full min-h-svh grid place-items-center bg-tm06-bg font-tm06-sans px-6 py-10">
<button type="button" data-tm06-open class="inline-flex items-center gap-2.5 min-h-11 px-5 rounded-xl text-sm font-semibold text-tm06-ink bg-tm06-btn border border-tm06-edge cursor-pointer transition-colors duration-200 hover:bg-tm06-btnhover hover:border-tm06-acc focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">
<span aria-hidden="true">🛍</span> Your bag <span class="grid place-items-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-tm06-acc text-xs font-bold text-tm06-onacc">2</span>
</button>
<dialog id="tm-06-dlg" aria-labelledby="tm-06-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(400px,100vw)] p-0 border-0 rounded-none text-tm06-ink bg-tm06-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-tm06-scrim motion-reduce:transition-none">
<div data-tm06-card class="flex flex-col h-full">
<header class="flex items-center justify-between px-5 pt-5 pb-4 border-b border-tm06-edge">
<h2 id="tm-06-title" class="text-lg font-bold text-tm06-hi">Your bag</h2>
<button type="button" data-tm06-close aria-label="Close bag" class="grid place-items-center size-9 rounded-lg border-0 bg-transparent text-tm06-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm06-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="px-5 py-4 border-b border-tm06-edge">
<p class="mb-2.5 text-[13px] text-tm06-mut"><b data-tm06-left class="font-semibold text-tm06-acc">$52</b> away from free shipping</p>
<div data-tm06-bar role="progressbar" aria-valuemin="0" aria-valuemax="200" aria-valuenow="148" aria-label="Progress to free shipping" class="h-1.5 rounded-full bg-tm06-well overflow-hidden">
<i data-tm06-fill style="width:74%" class="block h-full rounded-full bg-tm06-acc transition-[width] duration-300 motion-reduce:transition-none"></i>
</div>
</div>
<ul class="flex-1 overflow-auto list-none m-0 px-5 py-2">
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Merino Overshirt</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Forest · M</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="98" class="text-sm font-bold text-tm06-hi">$98</p>
</li>
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Canvas Field Cap</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Rust · OS</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="50" class="text-sm font-bold text-tm06-hi">$50</p>
</li>
</ul>
<footer class="px-5 pt-4 pb-5 border-t border-tm06-edge bg-tm06-foot">
<div class="flex items-baseline justify-between mb-3.5">
<span class="text-sm text-tm06-mut">Subtotal</span>
<b data-tm06-total aria-live="polite" class="text-[22px] text-white">$148</b>
</div>
<button type="button" class="w-full min-h-12 rounded-xl border-0 bg-tm06-acc text-[15px] font-bold text-tm06-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">Checkout</button>
<p class="mt-3 text-center text-[11.5px] text-tm06-faint">Shipping & taxes calculated at checkout</p>
</footer>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm06-bg: #0a0706;
--color-tm06-bg: oklch(0.15 0.015 30);
--color-tm06-card: #191210;
--color-tm06-card: oklch(0.21 0.018 30);
--color-tm06-foot: #140d0b;
--color-tm06-foot: oklch(0.18 0.015 30);
--color-tm06-well: #1a1310;
--color-tm06-well: oklch(0.2 0.018 30);
--color-tm06-btn: #1a1310;
--color-tm06-btn: oklch(0.2 0.018 30);
--color-tm06-btnhover: #241a16;
--color-tm06-btnhover: oklch(0.25 0.025 30);
--color-tm06-edge: #33251f;
--color-tm06-edge: oklch(0.3 0.025 30);
--color-tm06-stripeA: #241a15;
--color-tm06-stripeA: oklch(0.24 0.02 30);
--color-tm06-stripeB: #1c130f;
--color-tm06-stripeB: oklch(0.21 0.018 30);
--color-tm06-hi: #f7ede9;
--color-tm06-hi: oklch(0.95 0.012 30);
--color-tm06-ink: #e5d5cf;
--color-tm06-ink: oklch(0.89 0.018 30);
--color-tm06-mut: #b09a92;
--color-tm06-mut: oklch(0.73 0.025 30);
--color-tm06-faint: #8a746b;
--color-tm06-faint: oklch(0.6 0.025 30);
--color-tm06-acc: #f08a63;
--color-tm06-acc: oklch(0.74 0.15 40);
--color-tm06-onacc: #180a07;
--color-tm06-onacc: oklch(0.2 0.04 40);
--color-tm06-scrim: #0604038c;
--color-tm06-scrim: oklch(0.12 0.01 30 / 0.55);
--font-tm06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tm-06 {
width: 100%;
}
#tm-06-dlg::backdrop {
background: oklch(0.12 0.01 30 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-06-dlg[open]::backdrop {
background: oklch(0.12 0.01 30 / 0);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-06');
var dlg = root.querySelector('#tm-06-dlg');
var card = root.querySelector('[data-tm06-card]');
var THRESHOLD = 200;
root.querySelector('[data-tm06-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm06-close]').addEventListener('click', function () { dlg.close(); });
function recalc() {
var sum = 0;
root.querySelectorAll('[data-tm06-item]').forEach(function (li) {
var q = parseInt(li.querySelector('[data-tm06-qty]').textContent, 10);
var line = li.querySelector('[data-tm06-line]');
var base = parseInt(line.dataset.price, 10);
line.textContent = '$' + base * q;
sum += base * q;
});
root.querySelector('[data-tm06-total]').textContent = '$' + sum;
root.querySelector('[data-tm06-fill]').style.width = Math.min(100, sum / THRESHOLD * 100) + '%';
root.querySelector('[data-tm06-left]').textContent = '$' + Math.max(0, THRESHOLD - sum);
root.querySelector('[data-tm06-bar]').setAttribute('aria-valuenow', sum);
}
root.querySelectorAll('[data-tm06-step]').forEach(function (b) {
b.addEventListener('click', function () {
var span = b.parentNode.querySelector('[data-tm06-qty]');
span.textContent = Math.max(1, parseInt(span.textContent, 10) + parseInt(b.dataset.tm06Step, 10));
recalc();
});
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Shopping Cart Slide-Over Drawer with Dialog
Source: https://codefronts.com/components/tailwind-css-modals/tailwind-shopping-cart-slide-over-drawer-dialog/
A right-edge mini-cart that slides in over an inert page: line items, a free-shipping progress bar, a live subtotal and a checkout CTA. The slide is translate-x-full → open:translate-x-0 with open:starting:translate-x-full — in and out, with no JS timers and no transition-end listeners.
## HTML
```html
<section class="tm-06 w-full min-h-svh grid place-items-center bg-tm06-bg font-tm06-sans px-6 py-10">
<button type="button" data-tm06-open class="inline-flex items-center gap-2.5 min-h-11 px-5 rounded-xl text-sm font-semibold text-tm06-ink bg-tm06-btn border border-tm06-edge cursor-pointer transition-colors duration-200 hover:bg-tm06-btnhover hover:border-tm06-acc focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">
<span aria-hidden="true">🛍</span> Your bag <span class="grid place-items-center min-w-[22px] h-[22px] px-1.5 rounded-full bg-tm06-acc text-xs font-bold text-tm06-onacc">2</span>
</button>
<dialog id="tm-06-dlg" aria-labelledby="tm-06-title" class="ml-auto mr-0 my-0 h-full max-h-dvh w-[min(400px,100vw)] p-0 border-0 rounded-none text-tm06-ink bg-tm06-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-tm06-scrim motion-reduce:transition-none">
<div data-tm06-card class="flex flex-col h-full">
<header class="flex items-center justify-between px-5 pt-5 pb-4 border-b border-tm06-edge">
<h2 id="tm-06-title" class="text-lg font-bold text-tm06-hi">Your bag</h2>
<button type="button" data-tm06-close aria-label="Close bag" class="grid place-items-center size-9 rounded-lg border-0 bg-transparent text-tm06-mut text-[22px] leading-none cursor-pointer transition-colors duration-200 hover:bg-tm06-well hover:text-white focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:outline-offset-2">×</button>
</header>
<div class="px-5 py-4 border-b border-tm06-edge">
<p class="mb-2.5 text-[13px] text-tm06-mut"><b data-tm06-left class="font-semibold text-tm06-acc">$52</b> away from free shipping</p>
<div data-tm06-bar role="progressbar" aria-valuemin="0" aria-valuemax="200" aria-valuenow="148" aria-label="Progress to free shipping" class="h-1.5 rounded-full bg-tm06-well overflow-hidden">
<i data-tm06-fill style="width:74%" class="block h-full rounded-full bg-tm06-acc transition-[width] duration-300 motion-reduce:transition-none"></i>
</div>
</div>
<ul class="flex-1 overflow-auto list-none m-0 px-5 py-2">
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Merino Overshirt</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Forest · M</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="98" class="text-sm font-bold text-tm06-hi">$98</p>
</li>
<li data-tm06-item class="grid grid-cols-[56px_1fr_auto] items-center gap-3.5 py-4 border-b border-tm06-edge">
<div class="relative w-14 h-16 rounded-[9px] border border-tm06-edge bg-[repeating-linear-gradient(135deg,var(--color-tm06-stripeA)_0_8px,var(--color-tm06-stripeB)_8px_16px)] after:content-['shot'] after:absolute after:inset-0 after:grid after:place-items-center after:font-tm06-mono after:text-[9px] after:tracking-[0.1em] after:text-tm06-faint"></div>
<div>
<p class="text-sm font-semibold text-tm06-hi">Canvas Field Cap</p>
<p class="mt-0.5 mb-2 text-xs text-tm06-mut">Rust · OS</p>
<div class="inline-flex items-center rounded-lg border border-tm06-edge overflow-hidden">
<button type="button" data-tm06-step="-1" aria-label="Decrease quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">−</button>
<span data-tm06-qty aria-live="polite" class="min-w-9 text-center text-[13px] font-semibold text-white">1</span>
<button type="button" data-tm06-step="1" aria-label="Increase quantity" class="w-11 h-11 border-0 bg-tm06-well text-tm06-ink text-base cursor-pointer transition-colors duration-150 hover:bg-tm06-btnhover focus-visible:outline-2 focus-visible:outline-tm06-acc focus-visible:-outline-offset-2">+</button>
</div>
</div>
<p data-tm06-line data-price="50" class="text-sm font-bold text-tm06-hi">$50</p>
</li>
</ul>
<footer class="px-5 pt-4 pb-5 border-t border-tm06-edge bg-tm06-foot">
<div class="flex items-baseline justify-between mb-3.5">
<span class="text-sm text-tm06-mut">Subtotal</span>
<b data-tm06-total aria-live="polite" class="text-[22px] text-white">$148</b>
</div>
<button type="button" class="w-full min-h-12 rounded-xl border-0 bg-tm06-acc text-[15px] font-bold text-tm06-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">Checkout</button>
<p class="mt-3 text-center text-[11.5px] text-tm06-faint">Shipping & taxes calculated at checkout</p>
</footer>
</div>
</dialog>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tm06-bg: #0a0706;
--color-tm06-bg: oklch(0.15 0.015 30);
--color-tm06-card: #191210;
--color-tm06-card: oklch(0.21 0.018 30);
--color-tm06-foot: #140d0b;
--color-tm06-foot: oklch(0.18 0.015 30);
--color-tm06-well: #1a1310;
--color-tm06-well: oklch(0.2 0.018 30);
--color-tm06-btn: #1a1310;
--color-tm06-btn: oklch(0.2 0.018 30);
--color-tm06-btnhover: #241a16;
--color-tm06-btnhover: oklch(0.25 0.025 30);
--color-tm06-edge: #33251f;
--color-tm06-edge: oklch(0.3 0.025 30);
--color-tm06-stripeA: #241a15;
--color-tm06-stripeA: oklch(0.24 0.02 30);
--color-tm06-stripeB: #1c130f;
--color-tm06-stripeB: oklch(0.21 0.018 30);
--color-tm06-hi: #f7ede9;
--color-tm06-hi: oklch(0.95 0.012 30);
--color-tm06-ink: #e5d5cf;
--color-tm06-ink: oklch(0.89 0.018 30);
--color-tm06-mut: #b09a92;
--color-tm06-mut: oklch(0.73 0.025 30);
--color-tm06-faint: #8a746b;
--color-tm06-faint: oklch(0.6 0.025 30);
--color-tm06-acc: #f08a63;
--color-tm06-acc: oklch(0.74 0.15 40);
--color-tm06-onacc: #180a07;
--color-tm06-onacc: oklch(0.2 0.04 40);
--color-tm06-scrim: #0604038c;
--color-tm06-scrim: oklch(0.12 0.01 30 / 0.55);
--font-tm06-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tm06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tm-06 {
width: 100%;
}
#tm-06-dlg::backdrop {
background: oklch(0.12 0.01 30 / 0);
transition: background .34s, overlay .34s allow-discrete, display .34s allow-discrete;
}
@starting-style {
#tm-06-dlg[open]::backdrop {
background: oklch(0.12 0.01 30 / 0);
}
}
```
## JavaScript
```js
(function () {
var root = document.querySelector('.tm-06');
var dlg = root.querySelector('#tm-06-dlg');
var card = root.querySelector('[data-tm06-card]');
var THRESHOLD = 200;
root.querySelector('[data-tm06-open]').addEventListener('click', function () { dlg.showModal(); });
root.querySelector('[data-tm06-close]').addEventListener('click', function () { dlg.close(); });
function recalc() {
var sum = 0;
root.querySelectorAll('[data-tm06-item]').forEach(function (li) {
var q = parseInt(li.querySelector('[data-tm06-qty]').textContent, 10);
var line = li.querySelector('[data-tm06-line]');
var base = parseInt(line.dataset.price, 10);
line.textContent = '$' + base * q;
sum += base * q;
});
root.querySelector('[data-tm06-total]').textContent = '$' + sum;
root.querySelector('[data-tm06-fill]').style.width = Math.min(100, sum / THRESHOLD * 100) + '%';
root.querySelector('[data-tm06-left]').textContent = '$' + Math.max(0, THRESHOLD - sum);
root.querySelector('[data-tm06-bar]').setAttribute('aria-valuenow', sum);
}
root.querySelectorAll('[data-tm06-step]').forEach(function (b) {
b.addEventListener('click', function () {
var span = b.parentNode.querySelector('[data-tm06-qty]');
span.textContent = Math.max(1, parseInt(span.textContent, 10) + parseInt(b.dataset.tm06Step, 10));
recalc();
});
});
dlg.addEventListener('click', function (e) {
var r = card.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dlg.close();
});
})();
```How this works
A drawer is a dialog with different margins. ml-auto and h-full put it on the right edge; everything else — focus trap, Esc, inert background, focus restore — is the same showModal() contract as a centred modal. Hand-rolled off-canvas panels reimplement all four and usually get two of them wrong.
The exit animation is the reason for transition-discrete. Without it the drawer would slide in and then vanish on close, because display:none applies instantly. With it, display and overlay hold until the transform finishes.
Totals are computed, never duplicated. Each line stores its unit price in data-price; the controller multiplies, writes the line total, the subtotal, the progress width and the remaining-to-free-shipping figure from one pass.
Make it yours
- Left-hand drawer: swap
ml-autoformr-autoandtranslate-x-fullfor-translate-x-full. Two edits. - Free-shipping threshold lives in one constant in the controller and one
aria-valuemax. - Add a remove-line button and an empty-cart panel keyed off a
data-emptyattribute on the drawer.
Gotchas — read before shipping
h-fullplusmax-h-dvhmatters: a long cart without an internal scroll area pushes the checkout button off the bottom of the viewport with no way back.- The backdrop click test compares against the card's rect, not the dialog's — on a full-height drawer they are the same box, so the test is against the visible panel.
- Keep quantity controls at 44px. In a drawer they sit close to the edge of the screen, where thumb accuracy is worst.
aria-valuenowon the progress bar must be updated alongside the visual width, or screen-reader users hear a stale figure.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ · 117+ anim | 16.4+ · 17.5+ anim | 128+ · 129+ anim | 111+ · 117+ anim |
@starting-style and transition-behavior gate the slide; without them the drawer appears and disappears instantly, which is still fully functional.