01 / 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 Product Card with Parallax Hover Effect
The D2C product spotlight in utilities: the sneaker floats at translate-z-[50px], the Add-to-Cart button rides higher at translate-z-[60px], and a radial glare chases the cursor. The whole depth stack is declared on the elements that own it, and the port adds a working colourway picker — three real radios that re-hue the product with has-[[value=volt]:checked]:[--hue:110deg], no extra JS.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttc-01 w-full min-h-svh grid place-items-center bg-ttc01-bg bg-[radial-gradient(80%_90%_at_15%_10%,var(--color-ttc01-lobe1),transparent_60%),radial-gradient(70%_80%_at_90%_95%,var(--color-ttc01-lobe2),transparent_60%)] px-6 py-14 font-ttc01-sans text-ttc01-ink" aria-label="3D tilt product card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc01-card class="group/card relative w-full max-w-[19.5rem] rounded-[24px] border border-white/10 bg-linear-160 from-ttc01-card1 to-ttc01-card2 px-6 pt-6 pb-6 shadow-[0_30px_60px_-30px_oklch(0_0_0/0.8)] transform-3d [transform:perspective(1100px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none has-[[value=volt]:checked]:[--hue:110deg] has-[[value=tide]:checked]:[--hue:190deg]">
<div class="flex items-center justify-between translate-z-[35px]">
<span class="rounded-full border border-ttc01-acc/50 px-2.5 py-1.5 text-[10px] leading-none font-bold tracking-[0.14em] text-ttc01-acc">NEW DROP</span>
<span class="text-xs font-medium text-ttc01-mut">Free returns</span>
</div>
<div class="relative mx-auto mt-5 mb-4 aspect-[4/3] w-[94%] -rotate-6 translate-z-[50px] transition-[translate,rotate] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] group-data-live/card:-rotate-9 group-data-live/card:translate-z-[64px] motion-reduce:transition-none">
<div aria-hidden="true" class="absolute inset-[10%] rounded-full bg-ttc01-acc/40 blur-2xl"></div>
<img class="relative size-full rounded-2xl object-cover [filter:hue-rotate(var(--hue,0deg))_drop-shadow(0_24px_22px_oklch(0_0_0/0.55))] transition-[filter] duration-500" src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=640&auto=format&fit=crop" alt="Aurora Runner sneaker, side view" width="320" height="240" loading="lazy">
</div>
<h3 class="text-[22px] leading-tight font-bold tracking-[-0.02em] translate-z-[25px]">Aurora Runner</h3>
<div class="mt-1 flex items-center justify-between gap-3 translate-z-[25px]">
<p class="text-sm text-ttc01-mut">Limited colourway · <strong class="font-semibold text-ttc01-ink tabular-nums">$189</strong></p>
<fieldset class="flex items-center">
<legend class="sr-only">Colourway</legend>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="ember" checked aria-label="Ember" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-ember ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="volt" aria-label="Volt" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-volt ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="tide" aria-label="Tide" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-tide ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
</fieldset>
</div>
<button type="button" class="mt-4 h-12 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc01-acc to-ttc01-acc2 text-sm font-semibold text-ttc01-bg shadow-[0_14px_26px_-12px_var(--color-ttc01-acc2)] translate-z-[60px] transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ttc01-ink">Add to Cart</button>
<div aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-[inherit] bg-[radial-gradient(260px_circle_at_var(--mx,50%)_var(--my,50%),oklch(1_0_0/0.2),transparent_65%)] opacity-0 translate-z-[70px] transition-opacity duration-400 group-data-live/card:opacity-100"></div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/10 bg-white/5 px-3.5 py-2 font-ttc01-mono text-[11.5px] text-ttc01-mut">transform-3d · translate-z-[60px] · has-[:checked]:[--hue]</p>
</div>
</section><section class="ttc-01 w-full min-h-svh grid place-items-center bg-ttc01-bg bg-[radial-gradient(80%_90%_at_15%_10%,var(--color-ttc01-lobe1),transparent_60%),radial-gradient(70%_80%_at_90%_95%,var(--color-ttc01-lobe2),transparent_60%)] px-6 py-14 font-ttc01-sans text-ttc01-ink" aria-label="3D tilt product card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc01-card class="group/card relative w-full max-w-[19.5rem] rounded-[24px] border border-white/10 bg-linear-160 from-ttc01-card1 to-ttc01-card2 px-6 pt-6 pb-6 shadow-[0_30px_60px_-30px_oklch(0_0_0/0.8)] transform-3d [transform:perspective(1100px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none has-[[value=volt]:checked]:[--hue:110deg] has-[[value=tide]:checked]:[--hue:190deg]">
<div class="flex items-center justify-between translate-z-[35px]">
<span class="rounded-full border border-ttc01-acc/50 px-2.5 py-1.5 text-[10px] leading-none font-bold tracking-[0.14em] text-ttc01-acc">NEW DROP</span>
<span class="text-xs font-medium text-ttc01-mut">Free returns</span>
</div>
<div class="relative mx-auto mt-5 mb-4 aspect-[4/3] w-[94%] -rotate-6 translate-z-[50px] transition-[translate,rotate] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] group-data-live/card:-rotate-9 group-data-live/card:translate-z-[64px] motion-reduce:transition-none">
<div aria-hidden="true" class="absolute inset-[10%] rounded-full bg-ttc01-acc/40 blur-2xl"></div>
<img class="relative size-full rounded-2xl object-cover [filter:hue-rotate(var(--hue,0deg))_drop-shadow(0_24px_22px_oklch(0_0_0/0.55))] transition-[filter] duration-500" src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=640&auto=format&fit=crop" alt="Aurora Runner sneaker, side view" width="320" height="240" loading="lazy">
</div>
<h3 class="text-[22px] leading-tight font-bold tracking-[-0.02em] translate-z-[25px]">Aurora Runner</h3>
<div class="mt-1 flex items-center justify-between gap-3 translate-z-[25px]">
<p class="text-sm text-ttc01-mut">Limited colourway · <strong class="font-semibold text-ttc01-ink tabular-nums">$189</strong></p>
<fieldset class="flex items-center">
<legend class="sr-only">Colourway</legend>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="ember" checked aria-label="Ember" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-ember ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="volt" aria-label="Volt" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-volt ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="tide" aria-label="Tide" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-tide ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
</fieldset>
</div>
<button type="button" class="mt-4 h-12 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc01-acc to-ttc01-acc2 text-sm font-semibold text-ttc01-bg shadow-[0_14px_26px_-12px_var(--color-ttc01-acc2)] translate-z-[60px] transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ttc01-ink">Add to Cart</button>
<div aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-[inherit] bg-[radial-gradient(260px_circle_at_var(--mx,50%)_var(--my,50%),oklch(1_0_0/0.2),transparent_65%)] opacity-0 translate-z-[70px] transition-opacity duration-400 group-data-live/card:opacity-100"></div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/10 bg-white/5 px-3.5 py-2 font-ttc01-mono text-[11.5px] text-ttc01-mut">transform-3d · translate-z-[60px] · has-[:checked]:[--hue]</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-ttc01-bg: #0f0d18;
--color-ttc01-bg: oklch(0.15 0.02 280);
--color-ttc01-lobe1: #2e2244e6;
--color-ttc01-lobe1: oklch(0.28 0.06 290 / 0.9);
--color-ttc01-lobe2: #3a1d14cc;
--color-ttc01-lobe2: oklch(0.25 0.07 40 / 0.8);
--color-ttc01-card1: #25212f;
--color-ttc01-card1: oklch(0.24 0.03 285);
--color-ttc01-card2: #1a1722;
--color-ttc01-card2: oklch(0.18 0.025 280);
--color-ttc01-ink: #eef0f7;
--color-ttc01-ink: oklch(0.95 0.01 260);
--color-ttc01-mut: #b3b1c0;
--color-ttc01-mut: oklch(0.76 0.02 270);
--color-ttc01-acc: #f58d57;
--color-ttc01-acc: oklch(0.74 0.15 45);
--color-ttc01-acc2: #ea5d3d;
--color-ttc01-acc2: oklch(0.66 0.18 32);
--color-ttc01-ember: #e2403a;
--color-ttc01-ember: oklch(0.62 0.2 27);
--color-ttc01-volt: #9bc53d;
--color-ttc01-volt: oklch(0.76 0.17 125);
--color-ttc01-tide: #2fa6c9;
--color-ttc01-tide: oklch(0.67 0.12 225);
--font-ttc01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ttc01-mono: 'JetBrains 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-01 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttc01-bg: #0f0d18;
--color-ttc01-bg: oklch(0.15 0.02 280);
--color-ttc01-lobe1: #2e2244e6;
--color-ttc01-lobe1: oklch(0.28 0.06 290 / 0.9);
--color-ttc01-lobe2: #3a1d14cc;
--color-ttc01-lobe2: oklch(0.25 0.07 40 / 0.8);
--color-ttc01-card1: #25212f;
--color-ttc01-card1: oklch(0.24 0.03 285);
--color-ttc01-card2: #1a1722;
--color-ttc01-card2: oklch(0.18 0.025 280);
--color-ttc01-ink: #eef0f7;
--color-ttc01-ink: oklch(0.95 0.01 260);
--color-ttc01-mut: #b3b1c0;
--color-ttc01-mut: oklch(0.76 0.02 270);
--color-ttc01-acc: #f58d57;
--color-ttc01-acc: oklch(0.74 0.15 45);
--color-ttc01-acc2: #ea5d3d;
--color-ttc01-acc2: oklch(0.66 0.18 32);
--color-ttc01-ember: #e2403a;
--color-ttc01-ember: oklch(0.62 0.2 27);
--color-ttc01-volt: #9bc53d;
--color-ttc01-volt: oklch(0.76 0.17 125);
--color-ttc01-tide: #2fa6c9;
--color-ttc01-tide: oklch(0.67 0.12 225);
--font-ttc01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ttc01-mono: 'JetBrains 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-01 {
width: 100%;
min-height: 100svh;
}(() => {
document.querySelectorAll('[data-ttc01-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
// Touch + motion guards: everyone else gets the fully styled static card.
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 14; // degrees of tilt at the card's edge
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.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
card.setAttribute('data-live', '');
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live'); // re-arms the slow spring transition
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
})();(() => {
document.querySelectorAll('[data-ttc01-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
// Touch + motion guards: everyone else gets the fully styled static card.
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 14; // degrees of tilt at the card's edge
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.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
card.setAttribute('data-live', '');
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live'); // re-arms the slow spring transition
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-ttc01-bg: #0f0d18;
--color-ttc01-bg: oklch(0.15 0.02 280);
--color-ttc01-lobe1: #2e2244e6;
--color-ttc01-lobe1: oklch(0.28 0.06 290 / 0.9);
--color-ttc01-lobe2: #3a1d14cc;
--color-ttc01-lobe2: oklch(0.25 0.07 40 / 0.8);
--color-ttc01-card1: #25212f;
--color-ttc01-card1: oklch(0.24 0.03 285);
--color-ttc01-card2: #1a1722;
--color-ttc01-card2: oklch(0.18 0.025 280);
--color-ttc01-ink: #eef0f7;
--color-ttc01-ink: oklch(0.95 0.01 260);
--color-ttc01-mut: #b3b1c0;
--color-ttc01-mut: oklch(0.76 0.02 270);
--color-ttc01-acc: #f58d57;
--color-ttc01-acc: oklch(0.74 0.15 45);
--color-ttc01-acc2: #ea5d3d;
--color-ttc01-acc2: oklch(0.66 0.18 32);
--color-ttc01-ember: #e2403a;
--color-ttc01-ember: oklch(0.62 0.2 27);
--color-ttc01-volt: #9bc53d;
--color-ttc01-volt: oklch(0.76 0.17 125);
--color-ttc01-tide: #2fa6c9;
--color-ttc01-tide: oklch(0.67 0.12 225);
--font-ttc01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ttc01-mono: 'JetBrains 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-01 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttc01-bg: #0f0d18;
--color-ttc01-bg: oklch(0.15 0.02 280);
--color-ttc01-lobe1: #2e2244e6;
--color-ttc01-lobe1: oklch(0.28 0.06 290 / 0.9);
--color-ttc01-lobe2: #3a1d14cc;
--color-ttc01-lobe2: oklch(0.25 0.07 40 / 0.8);
--color-ttc01-card1: #25212f;
--color-ttc01-card1: oklch(0.24 0.03 285);
--color-ttc01-card2: #1a1722;
--color-ttc01-card2: oklch(0.18 0.025 280);
--color-ttc01-ink: #eef0f7;
--color-ttc01-ink: oklch(0.95 0.01 260);
--color-ttc01-mut: #b3b1c0;
--color-ttc01-mut: oklch(0.76 0.02 270);
--color-ttc01-acc: #f58d57;
--color-ttc01-acc: oklch(0.74 0.15 45);
--color-ttc01-acc2: #ea5d3d;
--color-ttc01-acc2: oklch(0.66 0.18 32);
--color-ttc01-ember: #e2403a;
--color-ttc01-ember: oklch(0.62 0.2 27);
--color-ttc01-volt: #9bc53d;
--color-ttc01-volt: oklch(0.76 0.17 125);
--color-ttc01-tide: #2fa6c9;
--color-ttc01-tide: oklch(0.67 0.12 225);
--font-ttc01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ttc01-mono: 'JetBrains 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-01 {
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=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=JetBrains+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=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=JetBrains+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Every
.thc-01__*rule became utilities on the element it styles; the only CSS left is@themetokens and the root size rule. .is-livebecame adata-liveattribute read bydata-live:andgroup-data-live/card:— the script no longer knows any class names.- UI upgrade from the CSS original: a working colourway picker — three 44px radio targets that re-hue the product through
has-[:checked]and a filter transition, zero extra JavaScript. - UI upgrade: the gradient placeholder became a real product photo with an accent halo behind it; the dead
tabindex="0"on the card was removed (the radios and CTA are the focus targets); muted copy rose from0.70to0.76lightness; the CTA grew to 48px.
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 Product Card with Parallax Hover Effect
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-tilt-product-card-parallax-hover/
The D2C product spotlight in utilities: the sneaker floats at translate-z-[50px], the Add-to-Cart button rides higher at translate-z-[60px], and a radial glare chases the cursor. The whole depth stack is declared on the elements that own it, and the port adds a working colourway picker — three real radios that re-hue the product with has-[[value=volt]:checked]:[--hue:110deg], no extra JS.
## HTML
```html
<section class="ttc-01 w-full min-h-svh grid place-items-center bg-ttc01-bg bg-[radial-gradient(80%_90%_at_15%_10%,var(--color-ttc01-lobe1),transparent_60%),radial-gradient(70%_80%_at_90%_95%,var(--color-ttc01-lobe2),transparent_60%)] px-6 py-14 font-ttc01-sans text-ttc01-ink" aria-label="3D tilt product card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc01-card class="group/card relative w-full max-w-[19.5rem] rounded-[24px] border border-white/10 bg-linear-160 from-ttc01-card1 to-ttc01-card2 px-6 pt-6 pb-6 shadow-[0_30px_60px_-30px_oklch(0_0_0/0.8)] transform-3d [transform:perspective(1100px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none has-[[value=volt]:checked]:[--hue:110deg] has-[[value=tide]:checked]:[--hue:190deg]">
<div class="flex items-center justify-between translate-z-[35px]">
<span class="rounded-full border border-ttc01-acc/50 px-2.5 py-1.5 text-[10px] leading-none font-bold tracking-[0.14em] text-ttc01-acc">NEW DROP</span>
<span class="text-xs font-medium text-ttc01-mut">Free returns</span>
</div>
<div class="relative mx-auto mt-5 mb-4 aspect-[4/3] w-[94%] -rotate-6 translate-z-[50px] transition-[translate,rotate] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] group-data-live/card:-rotate-9 group-data-live/card:translate-z-[64px] motion-reduce:transition-none">
<div aria-hidden="true" class="absolute inset-[10%] rounded-full bg-ttc01-acc/40 blur-2xl"></div>
<img class="relative size-full rounded-2xl object-cover [filter:hue-rotate(var(--hue,0deg))_drop-shadow(0_24px_22px_oklch(0_0_0/0.55))] transition-[filter] duration-500" src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=640&auto=format&fit=crop" alt="Aurora Runner sneaker, side view" width="320" height="240" loading="lazy">
</div>
<h3 class="text-[22px] leading-tight font-bold tracking-[-0.02em] translate-z-[25px]">Aurora Runner</h3>
<div class="mt-1 flex items-center justify-between gap-3 translate-z-[25px]">
<p class="text-sm text-ttc01-mut">Limited colourway · <strong class="font-semibold text-ttc01-ink tabular-nums">$189</strong></p>
<fieldset class="flex items-center">
<legend class="sr-only">Colourway</legend>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="ember" checked aria-label="Ember" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-ember ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="volt" aria-label="Volt" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-volt ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="tide" aria-label="Tide" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-tide ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
</fieldset>
</div>
<button type="button" class="mt-4 h-12 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc01-acc to-ttc01-acc2 text-sm font-semibold text-ttc01-bg shadow-[0_14px_26px_-12px_var(--color-ttc01-acc2)] translate-z-[60px] transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ttc01-ink">Add to Cart</button>
<div aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-[inherit] bg-[radial-gradient(260px_circle_at_var(--mx,50%)_var(--my,50%),oklch(1_0_0/0.2),transparent_65%)] opacity-0 translate-z-[70px] transition-opacity duration-400 group-data-live/card:opacity-100"></div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/10 bg-white/5 px-3.5 py-2 font-ttc01-mono text-[11.5px] text-ttc01-mut">transform-3d · translate-z-[60px] · has-[:checked]:[--hue]</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttc01-bg: #0f0d18;
--color-ttc01-bg: oklch(0.15 0.02 280);
--color-ttc01-lobe1: #2e2244e6;
--color-ttc01-lobe1: oklch(0.28 0.06 290 / 0.9);
--color-ttc01-lobe2: #3a1d14cc;
--color-ttc01-lobe2: oklch(0.25 0.07 40 / 0.8);
--color-ttc01-card1: #25212f;
--color-ttc01-card1: oklch(0.24 0.03 285);
--color-ttc01-card2: #1a1722;
--color-ttc01-card2: oklch(0.18 0.025 280);
--color-ttc01-ink: #eef0f7;
--color-ttc01-ink: oklch(0.95 0.01 260);
--color-ttc01-mut: #b3b1c0;
--color-ttc01-mut: oklch(0.76 0.02 270);
--color-ttc01-acc: #f58d57;
--color-ttc01-acc: oklch(0.74 0.15 45);
--color-ttc01-acc2: #ea5d3d;
--color-ttc01-acc2: oklch(0.66 0.18 32);
--color-ttc01-ember: #e2403a;
--color-ttc01-ember: oklch(0.62 0.2 27);
--color-ttc01-volt: #9bc53d;
--color-ttc01-volt: oklch(0.76 0.17 125);
--color-ttc01-tide: #2fa6c9;
--color-ttc01-tide: oklch(0.67 0.12 225);
--font-ttc01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ttc01-mono: 'JetBrains 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-01 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('[data-ttc01-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
// Touch + motion guards: everyone else gets the fully styled static card.
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 14; // degrees of tilt at the card's edge
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.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
card.setAttribute('data-live', '');
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live'); // re-arms the slow spring transition
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 Product Card with Parallax Hover Effect
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-tilt-product-card-parallax-hover/
The D2C product spotlight in utilities: the sneaker floats at translate-z-[50px], the Add-to-Cart button rides higher at translate-z-[60px], and a radial glare chases the cursor. The whole depth stack is declared on the elements that own it, and the port adds a working colourway picker — three real radios that re-hue the product with has-[[value=volt]:checked]:[--hue:110deg], no extra JS.
## HTML
```html
<section class="ttc-01 w-full min-h-svh grid place-items-center bg-ttc01-bg bg-[radial-gradient(80%_90%_at_15%_10%,var(--color-ttc01-lobe1),transparent_60%),radial-gradient(70%_80%_at_90%_95%,var(--color-ttc01-lobe2),transparent_60%)] px-6 py-14 font-ttc01-sans text-ttc01-ink" aria-label="3D tilt product card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc01-card class="group/card relative w-full max-w-[19.5rem] rounded-[24px] border border-white/10 bg-linear-160 from-ttc01-card1 to-ttc01-card2 px-6 pt-6 pb-6 shadow-[0_30px_60px_-30px_oklch(0_0_0/0.8)] transform-3d [transform:perspective(1100px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))] transition-transform duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] data-live:duration-80 data-live:ease-linear motion-reduce:transition-none has-[[value=volt]:checked]:[--hue:110deg] has-[[value=tide]:checked]:[--hue:190deg]">
<div class="flex items-center justify-between translate-z-[35px]">
<span class="rounded-full border border-ttc01-acc/50 px-2.5 py-1.5 text-[10px] leading-none font-bold tracking-[0.14em] text-ttc01-acc">NEW DROP</span>
<span class="text-xs font-medium text-ttc01-mut">Free returns</span>
</div>
<div class="relative mx-auto mt-5 mb-4 aspect-[4/3] w-[94%] -rotate-6 translate-z-[50px] transition-[translate,rotate] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.15)] group-data-live/card:-rotate-9 group-data-live/card:translate-z-[64px] motion-reduce:transition-none">
<div aria-hidden="true" class="absolute inset-[10%] rounded-full bg-ttc01-acc/40 blur-2xl"></div>
<img class="relative size-full rounded-2xl object-cover [filter:hue-rotate(var(--hue,0deg))_drop-shadow(0_24px_22px_oklch(0_0_0/0.55))] transition-[filter] duration-500" src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?q=80&w=640&auto=format&fit=crop" alt="Aurora Runner sneaker, side view" width="320" height="240" loading="lazy">
</div>
<h3 class="text-[22px] leading-tight font-bold tracking-[-0.02em] translate-z-[25px]">Aurora Runner</h3>
<div class="mt-1 flex items-center justify-between gap-3 translate-z-[25px]">
<p class="text-sm text-ttc01-mut">Limited colourway · <strong class="font-semibold text-ttc01-ink tabular-nums">$189</strong></p>
<fieldset class="flex items-center">
<legend class="sr-only">Colourway</legend>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="ember" checked aria-label="Ember" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-ember ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="volt" aria-label="Volt" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-volt ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
<label class="grid size-11 cursor-pointer place-items-center"><input type="radio" name="ttc01-colour" value="tide" aria-label="Tide" class="size-5 cursor-pointer appearance-none rounded-full bg-ttc01-tide ring-2 ring-transparent ring-offset-2 ring-offset-ttc01-card2 transition checked:ring-ttc01-ink focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ttc01-acc"></label>
</fieldset>
</div>
<button type="button" class="mt-4 h-12 w-full cursor-pointer rounded-xl bg-linear-120 from-ttc01-acc to-ttc01-acc2 text-sm font-semibold text-ttc01-bg shadow-[0_14px_26px_-12px_var(--color-ttc01-acc2)] translate-z-[60px] transition-[filter] hover:brightness-110 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-ttc01-ink">Add to Cart</button>
<div aria-hidden="true" class="pointer-events-none absolute inset-0 rounded-[inherit] bg-[radial-gradient(260px_circle_at_var(--mx,50%)_var(--my,50%),oklch(1_0_0/0.2),transparent_65%)] opacity-0 translate-z-[70px] transition-opacity duration-400 group-data-live/card:opacity-100"></div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/10 bg-white/5 px-3.5 py-2 font-ttc01-mono text-[11.5px] text-ttc01-mut">transform-3d · translate-z-[60px] · has-[:checked]:[--hue]</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttc01-bg: #0f0d18;
--color-ttc01-bg: oklch(0.15 0.02 280);
--color-ttc01-lobe1: #2e2244e6;
--color-ttc01-lobe1: oklch(0.28 0.06 290 / 0.9);
--color-ttc01-lobe2: #3a1d14cc;
--color-ttc01-lobe2: oklch(0.25 0.07 40 / 0.8);
--color-ttc01-card1: #25212f;
--color-ttc01-card1: oklch(0.24 0.03 285);
--color-ttc01-card2: #1a1722;
--color-ttc01-card2: oklch(0.18 0.025 280);
--color-ttc01-ink: #eef0f7;
--color-ttc01-ink: oklch(0.95 0.01 260);
--color-ttc01-mut: #b3b1c0;
--color-ttc01-mut: oklch(0.76 0.02 270);
--color-ttc01-acc: #f58d57;
--color-ttc01-acc: oklch(0.74 0.15 45);
--color-ttc01-acc2: #ea5d3d;
--color-ttc01-acc2: oklch(0.66 0.18 32);
--color-ttc01-ember: #e2403a;
--color-ttc01-ember: oklch(0.62 0.2 27);
--color-ttc01-volt: #9bc53d;
--color-ttc01-volt: oklch(0.76 0.17 125);
--color-ttc01-tide: #2fa6c9;
--color-ttc01-tide: oklch(0.67 0.12 225);
--font-ttc01-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ttc01-mono: 'JetBrains 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-01 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('[data-ttc01-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
// Touch + motion guards: everyone else gets the fully styled static card.
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 14; // degrees of tilt at the card's edge
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.style.setProperty('--mx', (px * 100).toFixed(1) + '%');
card.style.setProperty('--my', (py * 100).toFixed(1) + '%');
card.setAttribute('data-live', '');
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live'); // re-arms the slow spring transition
card.style.setProperty('--rx', '0deg');
card.style.setProperty('--ry', '0deg');
});
});
})();
```How this works
The bridge is unchanged; the stylesheet is gone. JavaScript converts the pointer into --rx/--ry (±14°) and --mx/--my (glare centre). The card consumes them in a single arbitrary transform, and transform-3d (preserve-3d) lets every child pick a plane:
<article class="group/card transform-3d
[transform:perspective(1100px)_rotateX(var(--rx,0deg))_rotateY(var(--ry,0deg))]
transition-transform duration-600 data-live:duration-80">State is an attribute. The script toggles data-live; data-live: swaps the easing on the card, and group-data-live/card: fades the glare in and lifts the product a further 14px. No class names live in the JS.
The colourway picker is pure CSS. Three radios sit in the card; has-[[value=volt]:checked]:[--hue:110deg] writes a hue on the card, and the image reads it in [filter:hue-rotate(var(--hue,0deg))] with a 500ms filter transition.
Make it yours
- Tune tilt strength with the
MAXconstant (10–12° reads premium; 20°+ reads playful). - Swap the photo for a transparent PNG cutout and change
object-covertoobject-contain—drop-shadow-[…]then follows the silhouette. - Push depth by raising the
translate-z-[…]values together; keep the CTA above the product so it reads as tappable. - Real colourways: replace the hue trick with one
<img>per variant andgroup-has-[[value=volt]:checked]/card:block.
Gotchas — read before shipping
- The glare MUST be
pointer-events-noneor it eats the hover and the tilt stutters. overflow-hiddenanywhere between the card and atranslate-zchild flattens the 3D stack — round the image, not a clipping wrapper.- Read
getBoundingClientRect()on every move; a cached rect breaks after any scroll or resize.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
The tilt itself (perspective, preserve-3d, translate) is universal; the floor is Tailwind v4's generated sheet. :has() drives the colourway picker.