07 / 36
All 36 in 36 Tailwind Stacked Cards
- 01Tailwind Scroll-Driven Scaling Card Stack
- 02Tailwind Swipe Card Deck Drag to Dismiss
- 03Tailwind Glassmorphism Testimonial Card Stack
- 04Tailwind Stacked Cards Expand Accordion on Click
- 05Tailwind Fan Out Photo Stack on Hover
- 06Tailwind 3D Perspective Tilt Card Stack
- 07Tailwind Multi Step Form Card Stack
- 08Tailwind Tabbed Card Stack Bring to Front
- 09Tailwind Click to Front Stacked Cards Cycle
- 10Tailwind Classic Card Deck Stack Shadow
- 11Tailwind Fan Spread Hand of Cards
- 12Tailwind Polaroid Photo Stack Scattered Rotate
- 13Tailwind Staircase Offset Stacked Cards Cascade
- 14Tailwind Cards Spread Out Horizontally on Hover
- 15Tailwind Stacked Cards Lift on Hover Sibling Push
- 16Tailwind Accordion Stacked Cards Slide Out Menu
- 17Tailwind Shuffle Cards Animation Loop Deck
- 18Tailwind File Folder Tab Stacked Cards
- 19Tailwind Scattered Photos Pinch to Centre on Hover
- 20Tailwind Mobile Swipe Card Stack Translate3d
- 21Tailwind Single Accordion Card Nested Layers
- 22Tailwind Isometric Tower Stacked Cards 3D Column
- 23Tailwind 3D Flip Card Stack Reveal Back
- 24Tailwind Card Stack Tilt on Hover Quadrant
- 25Tailwind Notification Stack Pile Dismiss
- 26Tailwind Receipt Roll Unroll Stacked Items
- 27Tailwind Scratch Card Reveal Canvas Mask
- 28Tailwind Stacked Cards Hover Reveal Spread
- 29Tailwind Scroll Activated Sticky Stacking Cards
- 30Tailwind Stacked Testimonial Slider Radio
- 31Tailwind Isometric Overlapping Stacked Cards
- 32Tailwind Bento Grid Stacked Cards Hover Fan
- 33Tailwind Perspective Card Deck Depth Tunnel
- 34Tailwind Peel Back Corner Card Reveal
- 35Tailwind Timeline Stacked Milestone Cards
- 36Tailwind Spiral Tower Rotating Stacked Cards
Tailwind Multi Step Form Card Stack
A wizard as a deck. The four [data-state] rules become four data-[state=…] variant sets typed on every card, so a step's whole life — waiting behind, in focus, sunk to the back — reads in one class list. Native constraint validation gates the advance, inert keeps focus out of the cards behind, and user-invalid: paints the error state without a single line of script.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="stk-07 w-full min-h-svh flex items-center justify-center px-5 py-12 font-stk07-sans bg-stk07-bg" aria-label="Multi-step form stack">
<div class="w-[min(400px,100%)]">
<div class="h-1.5 mb-4.5 rounded-full bg-stk07-track overflow-hidden">
<span class="block h-full w-1/3 rounded-full bg-stk07-accent transition-[width] duration-450 ease-stk07 motion-reduce:transition-none" id="stk-07-fill"></span>
</div>
<form class="relative h-85" id="stk-07-deck" novalidate>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 1 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your details</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Full name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Alex Rivera">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Email
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="email" placeholder="alex@studio.com">
</label>
<div class="mt-auto flex items-center justify-between">
<span></span>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 2 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your project</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Project name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Northwind redesign">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Budget
<select class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required>
<option value="">Select…</option><option>< $5k</option><option>$5k–$20k</option><option>$20k+</option>
</select>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 3 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Almost done</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Anything else?
<textarea class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20" rows="3" placeholder="Timeline, links, context…"></textarea>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="done" type="button">Submit ✓</button>
</div>
</fieldset>
<div class="absolute inset-0 z-4 grid place-items-center rounded-[20px] bg-white p-8 text-center text-[17px] font-semibold text-stk07-ink shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] hidden not-hidden:grid" id="stk-07-thanks" hidden>Thanks — we’ll be in touch within a day.</div>
</form>
</div>
</section><section class="stk-07 w-full min-h-svh flex items-center justify-center px-5 py-12 font-stk07-sans bg-stk07-bg" aria-label="Multi-step form stack">
<div class="w-[min(400px,100%)]">
<div class="h-1.5 mb-4.5 rounded-full bg-stk07-track overflow-hidden">
<span class="block h-full w-1/3 rounded-full bg-stk07-accent transition-[width] duration-450 ease-stk07 motion-reduce:transition-none" id="stk-07-fill"></span>
</div>
<form class="relative h-85" id="stk-07-deck" novalidate>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 1 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your details</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Full name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Alex Rivera">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Email
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="email" placeholder="alex@studio.com">
</label>
<div class="mt-auto flex items-center justify-between">
<span></span>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 2 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your project</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Project name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Northwind redesign">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Budget
<select class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required>
<option value="">Select…</option><option>< $5k</option><option>$5k–$20k</option><option>$20k+</option>
</select>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 3 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Almost done</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Anything else?
<textarea class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20" rows="3" placeholder="Timeline, links, context…"></textarea>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="done" type="button">Submit ✓</button>
</div>
</fieldset>
<div class="absolute inset-0 z-4 grid place-items-center rounded-[20px] bg-white p-8 text-center text-[17px] font-semibold text-stk07-ink shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] hidden not-hidden:grid" id="stk-07-thanks" hidden>Thanks — we’ll be in touch within a day.</div>
</form>
</div>
</section>@import "tailwindcss";
@theme {
--color-stk07-bg: #f2f3f7;
--color-stk07-bg: oklch(0.96 0.004 265);
--color-stk07-ink: #1a1d27;
--color-stk07-ink: oklch(0.24 0.014 265);
--color-stk07-label: #4a4f5e;
--color-stk07-label: oklch(0.44 0.014 265);
--color-stk07-line: #d6d9e2;
--color-stk07-line: oklch(0.88 0.008 265);
--color-stk07-field: #f8f9fb;
--color-stk07-field: oklch(0.98 0.003 265);
--color-stk07-track: #dde0e8;
--color-stk07-track: oklch(0.90 0.008 265);
--color-stk07-accent: #5b78e8;
--color-stk07-accent: oklch(0.62 0.17 262);
--color-stk07-bad: #dd5a44;
--color-stk07-bad: oklch(0.62 0.20 22);
--font-stk07-sans: 'Archivo', system-ui, sans-serif;
--font-stk07-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk07: cubic-bezier(0.3, 0.9, 0.3, 1);
}@import "tailwindcss";
@theme {
--color-stk07-bg: #f2f3f7;
--color-stk07-bg: oklch(0.96 0.004 265);
--color-stk07-ink: #1a1d27;
--color-stk07-ink: oklch(0.24 0.014 265);
--color-stk07-label: #4a4f5e;
--color-stk07-label: oklch(0.44 0.014 265);
--color-stk07-line: #d6d9e2;
--color-stk07-line: oklch(0.88 0.008 265);
--color-stk07-field: #f8f9fb;
--color-stk07-field: oklch(0.98 0.003 265);
--color-stk07-track: #dde0e8;
--color-stk07-track: oklch(0.90 0.008 265);
--color-stk07-accent: #5b78e8;
--color-stk07-accent: oklch(0.62 0.17 262);
--color-stk07-bad: #dd5a44;
--color-stk07-bad: oklch(0.62 0.20 22);
--font-stk07-sans: 'Archivo', system-ui, sans-serif;
--font-stk07-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk07: cubic-bezier(0.3, 0.9, 0.3, 1);
}(() => {
const root = document.querySelector('.stk-07');
if (!root) return;
const deck = root.querySelector('#stk-07-deck');
const cards = [...deck.querySelectorAll('fieldset')];
const fill = root.querySelector('#stk-07-fill');
const thanks = root.querySelector('#stk-07-thanks');
let i = 0;
/* The controller sets ONE attribute per card. What each state looks like
lives in the data-[state=…] variants on the cards themselves. */
function paint() {
cards.forEach((c, idx) => {
c.dataset.state = idx < i ? 'done' : idx === i ? 'active' : idx === i + 1 ? 'next' : 'next2';
c.toggleAttribute('inert', idx !== i);
c.setAttribute('aria-current', idx === i ? 'step' : 'false');
});
fill.style.width = ((i + 1) / cards.length * 100) + '%';
const first = cards[i].querySelector('input,select,textarea');
if (first) setTimeout(() => first.focus(), 260);
}
function valid(card) {
for (const f of card.querySelectorAll('input,select,textarea')) {
if (!f.checkValidity()) { f.reportValidity(); return false; }
}
return true;
}
deck.addEventListener('click', (e) => {
const act = e.target.closest('[data-act]');
if (!act) return;
if (act.dataset.act === 'next' && valid(cards[i])) { i = Math.min(i + 1, cards.length - 1); paint(); }
if (act.dataset.act === 'back') { i = Math.max(i - 1, 0); paint(); }
if (act.dataset.act === 'done' && valid(cards[i])) {
cards.forEach((c) => { c.dataset.state = 'done'; c.setAttribute('inert', ''); });
fill.style.width = '100%';
thanks.hidden = false;
thanks.focus?.();
}
});
paint();
})();(() => {
const root = document.querySelector('.stk-07');
if (!root) return;
const deck = root.querySelector('#stk-07-deck');
const cards = [...deck.querySelectorAll('fieldset')];
const fill = root.querySelector('#stk-07-fill');
const thanks = root.querySelector('#stk-07-thanks');
let i = 0;
/* The controller sets ONE attribute per card. What each state looks like
lives in the data-[state=…] variants on the cards themselves. */
function paint() {
cards.forEach((c, idx) => {
c.dataset.state = idx < i ? 'done' : idx === i ? 'active' : idx === i + 1 ? 'next' : 'next2';
c.toggleAttribute('inert', idx !== i);
c.setAttribute('aria-current', idx === i ? 'step' : 'false');
});
fill.style.width = ((i + 1) / cards.length * 100) + '%';
const first = cards[i].querySelector('input,select,textarea');
if (first) setTimeout(() => first.focus(), 260);
}
function valid(card) {
for (const f of card.querySelectorAll('input,select,textarea')) {
if (!f.checkValidity()) { f.reportValidity(); return false; }
}
return true;
}
deck.addEventListener('click', (e) => {
const act = e.target.closest('[data-act]');
if (!act) return;
if (act.dataset.act === 'next' && valid(cards[i])) { i = Math.min(i + 1, cards.length - 1); paint(); }
if (act.dataset.act === 'back') { i = Math.max(i - 1, 0); paint(); }
if (act.dataset.act === 'done' && valid(cards[i])) {
cards.forEach((c) => { c.dataset.state = 'done'; c.setAttribute('inert', ''); });
fill.style.width = '100%';
thanks.hidden = false;
thanks.focus?.();
}
});
paint();
})();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-stk07-bg: #f2f3f7;
--color-stk07-bg: oklch(0.96 0.004 265);
--color-stk07-ink: #1a1d27;
--color-stk07-ink: oklch(0.24 0.014 265);
--color-stk07-label: #4a4f5e;
--color-stk07-label: oklch(0.44 0.014 265);
--color-stk07-line: #d6d9e2;
--color-stk07-line: oklch(0.88 0.008 265);
--color-stk07-field: #f8f9fb;
--color-stk07-field: oklch(0.98 0.003 265);
--color-stk07-track: #dde0e8;
--color-stk07-track: oklch(0.90 0.008 265);
--color-stk07-accent: #5b78e8;
--color-stk07-accent: oklch(0.62 0.17 262);
--color-stk07-bad: #dd5a44;
--color-stk07-bad: oklch(0.62 0.20 22);
--font-stk07-sans: 'Archivo', system-ui, sans-serif;
--font-stk07-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk07: cubic-bezier(0.3, 0.9, 0.3, 1);
}@import "tailwindcss";
@theme {
--color-stk07-bg: #f2f3f7;
--color-stk07-bg: oklch(0.96 0.004 265);
--color-stk07-ink: #1a1d27;
--color-stk07-ink: oklch(0.24 0.014 265);
--color-stk07-label: #4a4f5e;
--color-stk07-label: oklch(0.44 0.014 265);
--color-stk07-line: #d6d9e2;
--color-stk07-line: oklch(0.88 0.008 265);
--color-stk07-field: #f8f9fb;
--color-stk07-field: oklch(0.98 0.003 265);
--color-stk07-track: #dde0e8;
--color-stk07-track: oklch(0.90 0.008 265);
--color-stk07-accent: #5b78e8;
--color-stk07-accent: oklch(0.62 0.17 262);
--color-stk07-bad: #dd5a44;
--color-stk07-bad: oklch(0.62 0.20 22);
--font-stk07-sans: 'Archivo', system-ui, sans-serif;
--font-stk07-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk07: cubic-bezier(0.3, 0.9, 0.3, 1);
}<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=Archivo:wdth,wght@75..125,400;75..125,600;75..125,800&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=Archivo:wdth,wght@75..125,400;75..125,600;75..125,800&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The webfonts load as
<link>tags insetup, not as an@importinside the stylesheet. Tailwind emits user@importrules after its own@layerblocks, and a late@importis invalid CSS — the browser drops it and the demo silently falls back to system fonts. - The four
[data-state="…"]rules become fourdata-[state=…]variant sets typed on every card, so the deck's whole choreography is legible in one class list. - The
:user-invalidrule becomesuser-invalid:border-stk07-bad— still the interaction-aware pseudo-class, not:invalid, so nothing turns red before the user has typed. pointer-events:noneon inactive steps is gone andinertdoes the job instead: it removes the subtree from the tab order and the accessibility tree, whichpointer-eventsnever did.- The focus glow on inputs is
focus:ring-3 focus:ring-stk07-accent/20— an opacity modifier on the theme token rather than a second hand-mixed colour. - The progress fill's width stays an inline style because it is computed at runtime; the transition that smooths it is a utility.
- The stylesheet is
@themeonly, which for a JS-driven demo is the strongest evidence that state belongs in attributes rather than classes.
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 Multi Step Form Card Stack
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-multi-step-form-card-stack/
A wizard as a deck. The four [data-state] rules become four data-[state=…] variant sets typed on every card, so a step's whole life — waiting behind, in focus, sunk to the back — reads in one class list. Native constraint validation gates the advance, inert keeps focus out of the cards behind, and user-invalid: paints the error state without a single line of script.
## HTML
```html
<section class="stk-07 w-full min-h-svh flex items-center justify-center px-5 py-12 font-stk07-sans bg-stk07-bg" aria-label="Multi-step form stack">
<div class="w-[min(400px,100%)]">
<div class="h-1.5 mb-4.5 rounded-full bg-stk07-track overflow-hidden">
<span class="block h-full w-1/3 rounded-full bg-stk07-accent transition-[width] duration-450 ease-stk07 motion-reduce:transition-none" id="stk-07-fill"></span>
</div>
<form class="relative h-85" id="stk-07-deck" novalidate>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 1 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your details</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Full name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Alex Rivera">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Email
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="email" placeholder="alex@studio.com">
</label>
<div class="mt-auto flex items-center justify-between">
<span></span>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 2 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your project</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Project name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Northwind redesign">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Budget
<select class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required>
<option value="">Select…</option><option>< $5k</option><option>$5k–$20k</option><option>$20k+</option>
</select>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 3 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Almost done</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Anything else?
<textarea class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20" rows="3" placeholder="Timeline, links, context…"></textarea>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="done" type="button">Submit ✓</button>
</div>
</fieldset>
<div class="absolute inset-0 z-4 grid place-items-center rounded-[20px] bg-white p-8 text-center text-[17px] font-semibold text-stk07-ink shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] hidden not-hidden:grid" id="stk-07-thanks" hidden>Thanks — we’ll be in touch within a day.</div>
</form>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk07-bg: #f2f3f7;
--color-stk07-bg: oklch(0.96 0.004 265);
--color-stk07-ink: #1a1d27;
--color-stk07-ink: oklch(0.24 0.014 265);
--color-stk07-label: #4a4f5e;
--color-stk07-label: oklch(0.44 0.014 265);
--color-stk07-line: #d6d9e2;
--color-stk07-line: oklch(0.88 0.008 265);
--color-stk07-field: #f8f9fb;
--color-stk07-field: oklch(0.98 0.003 265);
--color-stk07-track: #dde0e8;
--color-stk07-track: oklch(0.90 0.008 265);
--color-stk07-accent: #5b78e8;
--color-stk07-accent: oklch(0.62 0.17 262);
--color-stk07-bad: #dd5a44;
--color-stk07-bad: oklch(0.62 0.20 22);
--font-stk07-sans: 'Archivo', system-ui, sans-serif;
--font-stk07-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk07: cubic-bezier(0.3, 0.9, 0.3, 1);
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-07');
if (!root) return;
const deck = root.querySelector('#stk-07-deck');
const cards = [...deck.querySelectorAll('fieldset')];
const fill = root.querySelector('#stk-07-fill');
const thanks = root.querySelector('#stk-07-thanks');
let i = 0;
/* The controller sets ONE attribute per card. What each state looks like
lives in the data-[state=…] variants on the cards themselves. */
function paint() {
cards.forEach((c, idx) => {
c.dataset.state = idx < i ? 'done' : idx === i ? 'active' : idx === i + 1 ? 'next' : 'next2';
c.toggleAttribute('inert', idx !== i);
c.setAttribute('aria-current', idx === i ? 'step' : 'false');
});
fill.style.width = ((i + 1) / cards.length * 100) + '%';
const first = cards[i].querySelector('input,select,textarea');
if (first) setTimeout(() => first.focus(), 260);
}
function valid(card) {
for (const f of card.querySelectorAll('input,select,textarea')) {
if (!f.checkValidity()) { f.reportValidity(); return false; }
}
return true;
}
deck.addEventListener('click', (e) => {
const act = e.target.closest('[data-act]');
if (!act) return;
if (act.dataset.act === 'next' && valid(cards[i])) { i = Math.min(i + 1, cards.length - 1); paint(); }
if (act.dataset.act === 'back') { i = Math.max(i - 1, 0); paint(); }
if (act.dataset.act === 'done' && valid(cards[i])) {
cards.forEach((c) => { c.dataset.state = 'done'; c.setAttribute('inert', ''); });
fill.style.width = '100%';
thanks.hidden = false;
thanks.focus?.();
}
});
paint();
})();
```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 Multi Step Form Card Stack
Source: https://codefronts.com/components/tailwind-stacked-cards/tailwind-multi-step-form-card-stack/
A wizard as a deck. The four [data-state] rules become four data-[state=…] variant sets typed on every card, so a step's whole life — waiting behind, in focus, sunk to the back — reads in one class list. Native constraint validation gates the advance, inert keeps focus out of the cards behind, and user-invalid: paints the error state without a single line of script.
## HTML
```html
<section class="stk-07 w-full min-h-svh flex items-center justify-center px-5 py-12 font-stk07-sans bg-stk07-bg" aria-label="Multi-step form stack">
<div class="w-[min(400px,100%)]">
<div class="h-1.5 mb-4.5 rounded-full bg-stk07-track overflow-hidden">
<span class="block h-full w-1/3 rounded-full bg-stk07-accent transition-[width] duration-450 ease-stk07 motion-reduce:transition-none" id="stk-07-fill"></span>
</div>
<form class="relative h-85" id="stk-07-deck" novalidate>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 1 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your details</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Full name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Alex Rivera">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Email
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="email" placeholder="alex@studio.com">
</label>
<div class="mt-auto flex items-center justify-between">
<span></span>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 2 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Your project</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Project name
<input class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required type="text" placeholder="Northwind redesign">
</label>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Budget
<select class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20 user-invalid:border-stk07-bad" required>
<option value="">Select…</option><option>< $5k</option><option>$5k–$20k</option><option>$20k+</option>
</select>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="next" type="button">Continue</button>
</div>
</fieldset>
<fieldset class="absolute inset-0 flex flex-col gap-3 border-0 rounded-[20px] bg-white p-7 shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] transition-[transform,opacity] duration-500 ease-stk07 data-[state=active]:z-3 data-[state=active]:translate-y-0 data-[state=active]:scale-100 data-[state=active]:opacity-100 data-[state=next]:z-2 data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93] data-[state=next]:opacity-55 data-[state=next2]:z-1 data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86] data-[state=next2]:opacity-30 data-[state=done]:z-0 data-[state=done]:translate-y-20 data-[state=done]:scale-[0.8] data-[state=done]:opacity-0 motion-reduce:transition-none">
<p class="m-0 font-stk07-mono text-xs font-bold tracking-[0.12em] uppercase text-stk07-accent">Step 3 of 3</p>
<h3 class="m-0 text-[23px] font-extrabold tracking-[-0.02em] text-stk07-ink">Almost done</h3>
<label class="flex flex-col gap-1.5 text-[13px] font-semibold text-stk07-label">Anything else?
<textarea class="font-inherit font-normal px-3.5 py-2.75 rounded-[11px] border-[1.5px] border-stk07-line bg-stk07-field text-stk07-ink focus:outline-none focus:border-stk07-accent focus:ring-3 focus:ring-stk07-accent/20" rows="3" placeholder="Timeline, links, context…"></textarea>
</label>
<div class="mt-auto flex items-center justify-between">
<button class="rounded-[11px] border-0 bg-transparent text-sm font-semibold text-stk07-label cursor-pointer hover:text-stk07-ink focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="back" type="button">Back</button>
<button class="rounded-[11px] border-0 bg-stk07-accent px-5 py-2.75 text-sm font-bold text-white cursor-pointer transition-[transform,filter] duration-150 hover:brightness-110 active:scale-95 focus-visible:outline-3 focus-visible:outline-stk07-accent focus-visible:outline-offset-2" data-act="done" type="button">Submit ✓</button>
</div>
</fieldset>
<div class="absolute inset-0 z-4 grid place-items-center rounded-[20px] bg-white p-8 text-center text-[17px] font-semibold text-stk07-ink shadow-[0_24px_50px_-30px_rgba(20,24,50,0.5)] hidden not-hidden:grid" id="stk-07-thanks" hidden>Thanks — we’ll be in touch within a day.</div>
</form>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-stk07-bg: #f2f3f7;
--color-stk07-bg: oklch(0.96 0.004 265);
--color-stk07-ink: #1a1d27;
--color-stk07-ink: oklch(0.24 0.014 265);
--color-stk07-label: #4a4f5e;
--color-stk07-label: oklch(0.44 0.014 265);
--color-stk07-line: #d6d9e2;
--color-stk07-line: oklch(0.88 0.008 265);
--color-stk07-field: #f8f9fb;
--color-stk07-field: oklch(0.98 0.003 265);
--color-stk07-track: #dde0e8;
--color-stk07-track: oklch(0.90 0.008 265);
--color-stk07-accent: #5b78e8;
--color-stk07-accent: oklch(0.62 0.17 262);
--color-stk07-bad: #dd5a44;
--color-stk07-bad: oklch(0.62 0.20 22);
--font-stk07-sans: 'Archivo', system-ui, sans-serif;
--font-stk07-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-stk07: cubic-bezier(0.3, 0.9, 0.3, 1);
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.stk-07');
if (!root) return;
const deck = root.querySelector('#stk-07-deck');
const cards = [...deck.querySelectorAll('fieldset')];
const fill = root.querySelector('#stk-07-fill');
const thanks = root.querySelector('#stk-07-thanks');
let i = 0;
/* The controller sets ONE attribute per card. What each state looks like
lives in the data-[state=…] variants on the cards themselves. */
function paint() {
cards.forEach((c, idx) => {
c.dataset.state = idx < i ? 'done' : idx === i ? 'active' : idx === i + 1 ? 'next' : 'next2';
c.toggleAttribute('inert', idx !== i);
c.setAttribute('aria-current', idx === i ? 'step' : 'false');
});
fill.style.width = ((i + 1) / cards.length * 100) + '%';
const first = cards[i].querySelector('input,select,textarea');
if (first) setTimeout(() => first.focus(), 260);
}
function valid(card) {
for (const f of card.querySelectorAll('input,select,textarea')) {
if (!f.checkValidity()) { f.reportValidity(); return false; }
}
return true;
}
deck.addEventListener('click', (e) => {
const act = e.target.closest('[data-act]');
if (!act) return;
if (act.dataset.act === 'next' && valid(cards[i])) { i = Math.min(i + 1, cards.length - 1); paint(); }
if (act.dataset.act === 'back') { i = Math.max(i - 1, 0); paint(); }
if (act.dataset.act === 'done' && valid(cards[i])) {
cards.forEach((c) => { c.dataset.state = 'done'; c.setAttribute('inert', ''); });
fill.style.width = '100%';
thanks.hidden = false;
thanks.focus?.();
}
});
paint();
})();
```How this works
One state attribute, four variant sets. The controller's only styling job is to set data-state; the markup owns what each state looks like:
data-[state=active]:translate-y-0 data-[state=active]:scale-100
data-[state=next]:translate-y-6 data-[state=next]:scale-[0.93]
data-[state=next2]:translate-y-11 data-[state=next2]:scale-[0.86]
data-[state=done]:translate-y-20 data-[state=done]:opacity-0Every card carries the identical list, which means the deck's whole choreography can be reviewed in one place and changed once.
inert is the load-bearing accessibility attribute. opacity-0 and pointer-events-none hide a card from the eye and the mouse but leave its fields in the tab order, so a keyboard user tabs into an invisible form. inert removes the subtree from focus and from the accessibility tree in one attribute, and aria-current="step" marks the live card.
Validation is native. checkValidity() gates the advance and reportValidity() shows the browser's own message; user-invalid: does the styling. The important half is user-invalid rather than :invalid: an empty required field is invalid from first paint, and colouring it red before anyone has typed is hostile.
Focus follows the step. On advance the controller focuses the new card's first field after the transform settles, so the keyboard lands where the eye does.
Make it yours
- Add a step by copying a
<fieldset>; the controller reads the card list from the DOM, so nothing is hard-coded. - Change the peek depth via the
data-[state=next]anddata-[state=next2]transforms. - Swap native validation for your own by editing the
valid()hook — the only place validation is decided. - Recolour everything through
--color-stk07-accent: the progress fill, the focus ring, the buttons and the active step label all read it. - Retime the deck with
duration-500on the cards andduration-450on the progress fill.
Gotchas — read before shipping
- Move focus to the new step's first field, and do it after the transform, or the browser scrolls to a card that is still mid-flight.
- Use
inerton inactive steps.pointer-events-nonealone leaves hidden fields tabbable, which is a genuine trap. user-invalid:, notinvalid:. Required-but-empty fields are invalid before the user has done anything wrong.- Reserve the deck's height (
h-85). Absolutely-positioned steps have no intrinsic height and the panel would collapse. - The progress width has to be an inline style. It is
(i+1)/nat runtime and cannot be a static class. - Keep
novalidateon the form so the browser does not try to submit-and-validate the whole deck at once.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Constraint Validation and transforms are broadly supported (Chrome 60+, Safari 12+, Firefox 55+); :user-invalid needs Chrome 119+, Safari 16.5+, Firefox 88+ and degrades to no error tint. inert is Chrome 102+, Safari 15.5+, Firefox 112+.