03 / 15
All 15 in 15 Tailwind CSS 3D Tilt Hover Cards
- 01Tailwind 3D Tilt Product Card with Parallax Hover Effect
- 02Tailwind Glassmorphism 3D Tilt Profile Card
- 03Tailwind 3D Tilt Pricing Cards with Monthly / Annual Toggle
- 04Tailwind Holographic Card Effect with 3D Tilt
- 05Tailwind 3D Pop-Out Character Card with Cursor-Tracking Eyes
- 06Tailwind Mouse-Following Border Glow Cards
- 07Tailwind 3D Tilt Music Player Card with Spring Physics
- 08Tailwind Blog Card Hover Effect with Subtle 3D Tilt
- 09Tailwind Cyberpunk Neon Glow 3D Tilt Card
- 10Tailwind Dashboard KPI Card with 3D Hover Lift
- 11Tailwind Real Estate Listing Card with Grayscale-to-Colour Hover
- 12Tailwind 3D Flip Card with Mouse Tilt (Click to Flip)
- 13Tailwind 3D Floating Phone Mockup Card for App Landing Pages
- 14Tailwind Course Progress Card with 3D Tilt
- 15Pure Tailwind 3D Tilt Card — No JavaScript (:has() Hover Zones)
Tailwind 3D Tilt Pricing Cards with Monthly / Annual Toggle
A three-tier SaaS pricing row where every card tilts toward the cursor and the featured Pro tier adds a scale boost and a violet glow. The port finishes what the CSS original only promised: a working monthly/annual toggle whose prices swap through group-data-annual/root: variants, with tabular-nums keeping the digits rock-steady.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section data-ttc03-root class="ttc-03 group/root w-full min-h-svh grid place-items-center bg-ttc03-bg bg-[radial-gradient(90%_100%_at_50%_0%,var(--color-ttc03-lobe),transparent_60%)] px-5 py-14 font-ttc03-sans text-ttc03-ink" aria-label="3D tilt pricing cards demo">
<div class="grid w-full max-w-3xl justify-items-center gap-9">
<div class="grid justify-items-center gap-2">
<div role="group" aria-label="Billing period" class="inline-flex rounded-full bg-white/5 p-1 ring-1 ring-white/10">
<button type="button" data-period="monthly" aria-pressed="true" class="min-h-11 cursor-pointer rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Monthly</button>
<button type="button" data-period="annual" aria-pressed="false" class="inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Annual <span class="rounded-full bg-ttc03-glow/25 px-2 py-0.5 text-[11px] text-ttc03-badge">−20%</span></button>
</div>
<p aria-live="polite" class="text-xs text-ttc03-mut"><span class="group-data-annual/root:hidden">Billed monthly. Cancel anytime.</span><span class="hidden group-data-annual/root:inline">Billed yearly — two months free.</span></p>
</div>
<div class="flex w-full flex-wrap justify-center gap-5">
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Starter</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$12</span><span class="hidden group-data-annual/root:inline">$10</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>3 projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Community support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>1 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Starter</button>
</article>
<article data-ttc03-card data-featured class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-ttc03-glow/55 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_50px_-22px_var(--color-ttc03-glowsoft)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear data-live:[--pop:1.045] data-live:shadow-[0_28px_64px_-20px_var(--color-ttc03-glow)] motion-reduce:transition-none">
<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-linear-120 from-ttc03-glow to-ttc03-glow2 px-3 py-1.5 text-[10px] leading-none font-bold tracking-[0.12em] whitespace-nowrap text-white">MOST POPULAR</span>
<h3 class="text-[15px] font-semibold text-ttc03-mut">Pro</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$29</span><span class="hidden group-data-annual/root:inline">$23</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Priority support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>100 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc03-glow to-ttc03-glow2 text-sm font-semibold text-white transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Pro</button>
</article>
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Team</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$79</span><span class="hidden group-data-annual/root:inline">$63</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Everything in Pro</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>SSO & audit log</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Team</button>
</article>
</div>
</div>
</section><section data-ttc03-root class="ttc-03 group/root w-full min-h-svh grid place-items-center bg-ttc03-bg bg-[radial-gradient(90%_100%_at_50%_0%,var(--color-ttc03-lobe),transparent_60%)] px-5 py-14 font-ttc03-sans text-ttc03-ink" aria-label="3D tilt pricing cards demo">
<div class="grid w-full max-w-3xl justify-items-center gap-9">
<div class="grid justify-items-center gap-2">
<div role="group" aria-label="Billing period" class="inline-flex rounded-full bg-white/5 p-1 ring-1 ring-white/10">
<button type="button" data-period="monthly" aria-pressed="true" class="min-h-11 cursor-pointer rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Monthly</button>
<button type="button" data-period="annual" aria-pressed="false" class="inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Annual <span class="rounded-full bg-ttc03-glow/25 px-2 py-0.5 text-[11px] text-ttc03-badge">−20%</span></button>
</div>
<p aria-live="polite" class="text-xs text-ttc03-mut"><span class="group-data-annual/root:hidden">Billed monthly. Cancel anytime.</span><span class="hidden group-data-annual/root:inline">Billed yearly — two months free.</span></p>
</div>
<div class="flex w-full flex-wrap justify-center gap-5">
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Starter</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$12</span><span class="hidden group-data-annual/root:inline">$10</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>3 projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Community support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>1 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Starter</button>
</article>
<article data-ttc03-card data-featured class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-ttc03-glow/55 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_50px_-22px_var(--color-ttc03-glowsoft)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear data-live:[--pop:1.045] data-live:shadow-[0_28px_64px_-20px_var(--color-ttc03-glow)] motion-reduce:transition-none">
<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-linear-120 from-ttc03-glow to-ttc03-glow2 px-3 py-1.5 text-[10px] leading-none font-bold tracking-[0.12em] whitespace-nowrap text-white">MOST POPULAR</span>
<h3 class="text-[15px] font-semibold text-ttc03-mut">Pro</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$29</span><span class="hidden group-data-annual/root:inline">$23</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Priority support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>100 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc03-glow to-ttc03-glow2 text-sm font-semibold text-white transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Pro</button>
</article>
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Team</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$79</span><span class="hidden group-data-annual/root:inline">$63</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Everything in Pro</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>SSO & audit log</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Team</button>
</article>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-ttc03-bg: #0e0c17;
--color-ttc03-bg: oklch(0.15 0.02 285);
--color-ttc03-lobe: #2c1d4dcc;
--color-ttc03-lobe: oklch(0.24 0.06 295 / 0.8);
--color-ttc03-card1: #1f1b2b;
--color-ttc03-card1: oklch(0.22 0.03 290);
--color-ttc03-card2: #16131f;
--color-ttc03-card2: oklch(0.18 0.025 285);
--color-ttc03-ink: #eeedf6;
--color-ttc03-ink: oklch(0.95 0.01 280);
--color-ttc03-mut: #bdb9cc;
--color-ttc03-mut: oklch(0.8 0.02 285);
--color-ttc03-glow: #8a5cf0;
--color-ttc03-glow: oklch(0.62 0.21 295);
--color-ttc03-glowsoft: #8a5cf08c;
--color-ttc03-glowsoft: oklch(0.62 0.21 295 / 0.55);
--color-ttc03-glow2: #b048c0;
--color-ttc03-glow2: oklch(0.56 0.2 330);
--color-ttc03-badge: #d9c8ff;
--color-ttc03-badge: oklch(0.86 0.08 295);
--font-ttc03-sans: 'Geist', system-ui, sans-serif;
--font-ttc03-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ttc-03 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttc03-bg: #0e0c17;
--color-ttc03-bg: oklch(0.15 0.02 285);
--color-ttc03-lobe: #2c1d4dcc;
--color-ttc03-lobe: oklch(0.24 0.06 295 / 0.8);
--color-ttc03-card1: #1f1b2b;
--color-ttc03-card1: oklch(0.22 0.03 290);
--color-ttc03-card2: #16131f;
--color-ttc03-card2: oklch(0.18 0.025 285);
--color-ttc03-ink: #eeedf6;
--color-ttc03-ink: oklch(0.95 0.01 280);
--color-ttc03-mut: #bdb9cc;
--color-ttc03-mut: oklch(0.8 0.02 285);
--color-ttc03-glow: #8a5cf0;
--color-ttc03-glow: oklch(0.62 0.21 295);
--color-ttc03-glowsoft: #8a5cf08c;
--color-ttc03-glowsoft: oklch(0.62 0.21 295 / 0.55);
--color-ttc03-glow2: #b048c0;
--color-ttc03-glow2: oklch(0.56 0.2 330);
--color-ttc03-badge: #d9c8ff;
--color-ttc03-badge: oklch(0.86 0.08 295);
--font-ttc03-sans: 'Geist', system-ui, sans-serif;
--font-ttc03-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ttc-03 {
width: 100%;
min-height: 100svh;
}(() => {
document.querySelectorAll('[data-ttc03-root]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
// Billing toggle: one attribute on the root, aria-pressed on the buttons. Prices swap in CSS.
const btns = root.querySelectorAll('[data-period]');
btns.forEach((b) => b.addEventListener('click', () => {
const annual = b.dataset.period === 'annual';
root.toggleAttribute('data-annual', annual);
btns.forEach((x) => x.setAttribute('aria-pressed', String(x === b)));
}));
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 9;
root.querySelectorAll('[data-ttc03-card]').forEach((card) => { // adding a 4th tier is pure HTML
card.addEventListener('pointermove', (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
card.setAttribute('data-live', ''); // featured card also picks up --pop here
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
});
})();(() => {
document.querySelectorAll('[data-ttc03-root]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
// Billing toggle: one attribute on the root, aria-pressed on the buttons. Prices swap in CSS.
const btns = root.querySelectorAll('[data-period]');
btns.forEach((b) => b.addEventListener('click', () => {
const annual = b.dataset.period === 'annual';
root.toggleAttribute('data-annual', annual);
btns.forEach((x) => x.setAttribute('aria-pressed', String(x === b)));
}));
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 9;
root.querySelectorAll('[data-ttc03-card]').forEach((card) => { // adding a 4th tier is pure HTML
card.addEventListener('pointermove', (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
card.setAttribute('data-live', ''); // featured card also picks up --pop here
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
});
})();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-ttc03-bg: #0e0c17;
--color-ttc03-bg: oklch(0.15 0.02 285);
--color-ttc03-lobe: #2c1d4dcc;
--color-ttc03-lobe: oklch(0.24 0.06 295 / 0.8);
--color-ttc03-card1: #1f1b2b;
--color-ttc03-card1: oklch(0.22 0.03 290);
--color-ttc03-card2: #16131f;
--color-ttc03-card2: oklch(0.18 0.025 285);
--color-ttc03-ink: #eeedf6;
--color-ttc03-ink: oklch(0.95 0.01 280);
--color-ttc03-mut: #bdb9cc;
--color-ttc03-mut: oklch(0.8 0.02 285);
--color-ttc03-glow: #8a5cf0;
--color-ttc03-glow: oklch(0.62 0.21 295);
--color-ttc03-glowsoft: #8a5cf08c;
--color-ttc03-glowsoft: oklch(0.62 0.21 295 / 0.55);
--color-ttc03-glow2: #b048c0;
--color-ttc03-glow2: oklch(0.56 0.2 330);
--color-ttc03-badge: #d9c8ff;
--color-ttc03-badge: oklch(0.86 0.08 295);
--font-ttc03-sans: 'Geist', system-ui, sans-serif;
--font-ttc03-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ttc-03 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttc03-bg: #0e0c17;
--color-ttc03-bg: oklch(0.15 0.02 285);
--color-ttc03-lobe: #2c1d4dcc;
--color-ttc03-lobe: oklch(0.24 0.06 295 / 0.8);
--color-ttc03-card1: #1f1b2b;
--color-ttc03-card1: oklch(0.22 0.03 290);
--color-ttc03-card2: #16131f;
--color-ttc03-card2: oklch(0.18 0.025 285);
--color-ttc03-ink: #eeedf6;
--color-ttc03-ink: oklch(0.95 0.01 280);
--color-ttc03-mut: #bdb9cc;
--color-ttc03-mut: oklch(0.8 0.02 285);
--color-ttc03-glow: #8a5cf0;
--color-ttc03-glow: oklch(0.62 0.21 295);
--color-ttc03-glowsoft: #8a5cf08c;
--color-ttc03-glowsoft: oklch(0.62 0.21 295 / 0.55);
--color-ttc03-glow2: #b048c0;
--color-ttc03-glow2: oklch(0.56 0.2 330);
--color-ttc03-badge: #d9c8ff;
--color-ttc03-badge: oklch(0.86 0.08 295);
--font-ttc03-sans: 'Geist', system-ui, sans-serif;
--font-ttc03-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ttc-03 {
width: 100%;
min-height: 100svh;
}<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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.thc-03__card--featured.is-live{--pop:1.045}becamedata-live:[--pop:1.045]on the featured card — the scale boost is still a variable inside the one transform.- The
libackground-gradient bullets became inline check icons witharia-hidden, so the feature list reads cleanly and re-themes viatext-ttc03-glow. - UI upgrade from the CSS original: the monthly/annual switch the original only mentioned now works — a 44px segmented control with
aria-pressed, a −20% badge, a polite live region, and prices that swap without layout shift thanks totabular-nums. - UI upgrade: buttons rose from 40px to 44px, feature copy from 70% white to
oklch(0.80), and the three cards lost the deadtabindex="0"that doubled every tab stop.
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 3D Tilt Pricing Cards with Monthly / Annual Toggle
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-tilt-pricing-cards-monthly-annual-toggle/
A three-tier SaaS pricing row where every card tilts toward the cursor and the featured Pro tier adds a scale boost and a violet glow. The port finishes what the CSS original only promised: a working monthly/annual toggle whose prices swap through group-data-annual/root: variants, with tabular-nums keeping the digits rock-steady.
## HTML
```html
<section data-ttc03-root class="ttc-03 group/root w-full min-h-svh grid place-items-center bg-ttc03-bg bg-[radial-gradient(90%_100%_at_50%_0%,var(--color-ttc03-lobe),transparent_60%)] px-5 py-14 font-ttc03-sans text-ttc03-ink" aria-label="3D tilt pricing cards demo">
<div class="grid w-full max-w-3xl justify-items-center gap-9">
<div class="grid justify-items-center gap-2">
<div role="group" aria-label="Billing period" class="inline-flex rounded-full bg-white/5 p-1 ring-1 ring-white/10">
<button type="button" data-period="monthly" aria-pressed="true" class="min-h-11 cursor-pointer rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Monthly</button>
<button type="button" data-period="annual" aria-pressed="false" class="inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Annual <span class="rounded-full bg-ttc03-glow/25 px-2 py-0.5 text-[11px] text-ttc03-badge">−20%</span></button>
</div>
<p aria-live="polite" class="text-xs text-ttc03-mut"><span class="group-data-annual/root:hidden">Billed monthly. Cancel anytime.</span><span class="hidden group-data-annual/root:inline">Billed yearly — two months free.</span></p>
</div>
<div class="flex w-full flex-wrap justify-center gap-5">
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Starter</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$12</span><span class="hidden group-data-annual/root:inline">$10</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>3 projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Community support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>1 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Starter</button>
</article>
<article data-ttc03-card data-featured class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-ttc03-glow/55 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_50px_-22px_var(--color-ttc03-glowsoft)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear data-live:[--pop:1.045] data-live:shadow-[0_28px_64px_-20px_var(--color-ttc03-glow)] motion-reduce:transition-none">
<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-linear-120 from-ttc03-glow to-ttc03-glow2 px-3 py-1.5 text-[10px] leading-none font-bold tracking-[0.12em] whitespace-nowrap text-white">MOST POPULAR</span>
<h3 class="text-[15px] font-semibold text-ttc03-mut">Pro</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$29</span><span class="hidden group-data-annual/root:inline">$23</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Priority support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>100 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc03-glow to-ttc03-glow2 text-sm font-semibold text-white transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Pro</button>
</article>
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Team</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$79</span><span class="hidden group-data-annual/root:inline">$63</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Everything in Pro</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>SSO & audit log</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Team</button>
</article>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttc03-bg: #0e0c17;
--color-ttc03-bg: oklch(0.15 0.02 285);
--color-ttc03-lobe: #2c1d4dcc;
--color-ttc03-lobe: oklch(0.24 0.06 295 / 0.8);
--color-ttc03-card1: #1f1b2b;
--color-ttc03-card1: oklch(0.22 0.03 290);
--color-ttc03-card2: #16131f;
--color-ttc03-card2: oklch(0.18 0.025 285);
--color-ttc03-ink: #eeedf6;
--color-ttc03-ink: oklch(0.95 0.01 280);
--color-ttc03-mut: #bdb9cc;
--color-ttc03-mut: oklch(0.8 0.02 285);
--color-ttc03-glow: #8a5cf0;
--color-ttc03-glow: oklch(0.62 0.21 295);
--color-ttc03-glowsoft: #8a5cf08c;
--color-ttc03-glowsoft: oklch(0.62 0.21 295 / 0.55);
--color-ttc03-glow2: #b048c0;
--color-ttc03-glow2: oklch(0.56 0.2 330);
--color-ttc03-badge: #d9c8ff;
--color-ttc03-badge: oklch(0.86 0.08 295);
--font-ttc03-sans: 'Geist', system-ui, sans-serif;
--font-ttc03-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ttc-03 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('[data-ttc03-root]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
// Billing toggle: one attribute on the root, aria-pressed on the buttons. Prices swap in CSS.
const btns = root.querySelectorAll('[data-period]');
btns.forEach((b) => b.addEventListener('click', () => {
const annual = b.dataset.period === 'annual';
root.toggleAttribute('data-annual', annual);
btns.forEach((x) => x.setAttribute('aria-pressed', String(x === b)));
}));
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 9;
root.querySelectorAll('[data-ttc03-card]').forEach((card) => { // adding a 4th tier is pure HTML
card.addEventListener('pointermove', (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
card.setAttribute('data-live', ''); // featured card also picks up --pop here
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
});
})();
```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 3D Tilt Pricing Cards with Monthly / Annual Toggle
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-tilt-pricing-cards-monthly-annual-toggle/
A three-tier SaaS pricing row where every card tilts toward the cursor and the featured Pro tier adds a scale boost and a violet glow. The port finishes what the CSS original only promised: a working monthly/annual toggle whose prices swap through group-data-annual/root: variants, with tabular-nums keeping the digits rock-steady.
## HTML
```html
<section data-ttc03-root class="ttc-03 group/root w-full min-h-svh grid place-items-center bg-ttc03-bg bg-[radial-gradient(90%_100%_at_50%_0%,var(--color-ttc03-lobe),transparent_60%)] px-5 py-14 font-ttc03-sans text-ttc03-ink" aria-label="3D tilt pricing cards demo">
<div class="grid w-full max-w-3xl justify-items-center gap-9">
<div class="grid justify-items-center gap-2">
<div role="group" aria-label="Billing period" class="inline-flex rounded-full bg-white/5 p-1 ring-1 ring-white/10">
<button type="button" data-period="monthly" aria-pressed="true" class="min-h-11 cursor-pointer rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Monthly</button>
<button type="button" data-period="annual" aria-pressed="false" class="inline-flex min-h-11 cursor-pointer items-center gap-2 rounded-full px-5 text-sm font-semibold text-ttc03-mut transition-colors aria-pressed:bg-white aria-pressed:text-ttc03-bg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttc03-glow">Annual <span class="rounded-full bg-ttc03-glow/25 px-2 py-0.5 text-[11px] text-ttc03-badge">−20%</span></button>
</div>
<p aria-live="polite" class="text-xs text-ttc03-mut"><span class="group-data-annual/root:hidden">Billed monthly. Cancel anytime.</span><span class="hidden group-data-annual/root:inline">Billed yearly — two months free.</span></p>
</div>
<div class="flex w-full flex-wrap justify-center gap-5">
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Starter</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$12</span><span class="hidden group-data-annual/root:inline">$10</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>3 projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Community support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>1 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Starter</button>
</article>
<article data-ttc03-card data-featured class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-ttc03-glow/55 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_50px_-22px_var(--color-ttc03-glowsoft)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear data-live:[--pop:1.045] data-live:shadow-[0_28px_64px_-20px_var(--color-ttc03-glow)] motion-reduce:transition-none">
<span class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-linear-120 from-ttc03-glow to-ttc03-glow2 px-3 py-1.5 text-[10px] leading-none font-bold tracking-[0.12em] whitespace-nowrap text-white">MOST POPULAR</span>
<h3 class="text-[15px] font-semibold text-ttc03-mut">Pro</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$29</span><span class="hidden group-data-annual/root:inline">$23</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited projects</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Priority support</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>100 GB storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc03-glow to-ttc03-glow2 text-sm font-semibold text-white transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Pro</button>
</article>
<article data-ttc03-card class="relative flex-[1_1_13rem] max-w-60 rounded-[20px] border border-white/10 bg-linear-165 from-ttc03-card1 to-ttc03-card2 p-6 shadow-[0_20px_44px_-26px_oklch(0_0_0/0.8)] [transform:perspective(1000px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))_scale(var(--pop,1))] transition-[transform,box-shadow] duration-550 ease-[cubic-bezier(0.2,0.9,0.3,1.1)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none">
<h3 class="text-[15px] font-semibold text-ttc03-mut">Team</h3>
<p class="mt-2 tabular-nums"><strong class="text-[38px] font-extrabold tracking-[-0.03em]"><span class="group-data-annual/root:hidden">$79</span><span class="hidden group-data-annual/root:inline">$63</span></strong><span class="text-sm text-ttc03-mut">/mo</span></p>
<ul class="mt-4 grid gap-2.5 text-[13.5px] text-ttc03-mut">
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Everything in Pro</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>SSO & audit log</li>
<li class="flex gap-2.5"><svg aria-hidden="true" viewBox="0 0 16 16" class="mt-0.5 size-4 flex-none text-ttc03-glow"><path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited storage</li>
</ul>
<button type="button" class="mt-6 min-h-11 w-full cursor-pointer rounded-xl border border-white/25 text-sm font-semibold transition-colors hover:bg-white/8 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-white">Choose Team</button>
</article>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttc03-bg: #0e0c17;
--color-ttc03-bg: oklch(0.15 0.02 285);
--color-ttc03-lobe: #2c1d4dcc;
--color-ttc03-lobe: oklch(0.24 0.06 295 / 0.8);
--color-ttc03-card1: #1f1b2b;
--color-ttc03-card1: oklch(0.22 0.03 290);
--color-ttc03-card2: #16131f;
--color-ttc03-card2: oklch(0.18 0.025 285);
--color-ttc03-ink: #eeedf6;
--color-ttc03-ink: oklch(0.95 0.01 280);
--color-ttc03-mut: #bdb9cc;
--color-ttc03-mut: oklch(0.8 0.02 285);
--color-ttc03-glow: #8a5cf0;
--color-ttc03-glow: oklch(0.62 0.21 295);
--color-ttc03-glowsoft: #8a5cf08c;
--color-ttc03-glowsoft: oklch(0.62 0.21 295 / 0.55);
--color-ttc03-glow2: #b048c0;
--color-ttc03-glow2: oklch(0.56 0.2 330);
--color-ttc03-badge: #d9c8ff;
--color-ttc03-badge: oklch(0.86 0.08 295);
--font-ttc03-sans: 'Geist', system-ui, sans-serif;
--font-ttc03-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ttc-03 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('[data-ttc03-root]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
// Billing toggle: one attribute on the root, aria-pressed on the buttons. Prices swap in CSS.
const btns = root.querySelectorAll('[data-period]');
btns.forEach((b) => b.addEventListener('click', () => {
const annual = b.dataset.period === 'annual';
root.toggleAttribute('data-annual', annual);
btns.forEach((x) => x.setAttribute('aria-pressed', String(x === b)));
}));
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 9;
root.querySelectorAll('[data-ttc03-card]').forEach((card) => { // adding a 4th tier is pure HTML
card.addEventListener('pointermove', (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width, py = (e.clientY - r.top) / r.height;
card.style.setProperty('--rx', ((0.5 - py) * MAX).toFixed(2) + 'deg');
card.style.setProperty('--ry', ((px - 0.5) * MAX).toFixed(2) + 'deg');
card.setAttribute('data-live', ''); // featured card also picks up --pop here
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
});
})();
```How this works
One transform, two inputs. Rotation comes from the pointer; scale comes from --pop, which only the featured card flips to 1.045 via data-live:[--pop:1.045]. Keeping scale inside the same transform value is what stops Safari from promoting a second layer and flickering.
The toggle is attribute-driven. The section is group/root; the script sets data-annual on it and flips aria-pressed on the two buttons. Every price holds both values:
<span class="group-data-annual/root:hidden">$12</span>
<span class="hidden group-data-annual/root:inline">$10</span>so the JavaScript never writes text — and the aria-live note under the toggle announces the change for screen readers.
Make it yours
- Feature a different tier by moving
data-featured, the glow and the badge to another card. - Tighten
MAXto 6–8° if the pricing row sits above the fold where big motion distracts. - Re-brand the glow in one place:
--color-ttc03-glow. - Add a currency switch the same way: a second root attribute and
group-data-eur/root:spans.
Gotchas — read before shipping
- Never add a separate
scale-*utility on hover — it writes the individualscaleproperty and fights the tilt's timing. - Keep the featured scale under ~1.05 or it overlaps neighbours at narrow widths.
- The toggle buttons must be real
<button>s witharia-pressed; a styled checkbox reads as a form field, not a view switch.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Nothing beyond the Tailwind v4 floor; aria-pressed: and group-data-* are plain attribute selectors.