05 / 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 Pop-Out Character Card with Cursor-Tracking Eyes
The scroll-stopping agency trick: a mascot whose head rises OUTSIDE the card, floating at translate-z-[45px] and lifting to 70px on hover while the card tilts beneath it. The port gives the character a personality upgrade — pupils that follow the cursor and an idle blink — using the same custom-property bridge that drives the tilt.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="ttc-05 w-full min-h-svh grid place-items-center bg-ttc05-bg bg-[radial-gradient(80%_90%_at_80%_10%,var(--color-ttc05-lobe1),transparent_60%),radial-gradient(70%_80%_at_10%_90%,var(--color-ttc05-lobe2),transparent_60%)] px-6 pt-28 pb-14 font-ttc05-sans text-white" aria-label="3D pop-out character card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc05-card class="group/card relative h-64 w-full max-w-[18rem] overflow-visible rounded-[24px] transform-3d [transform:perspective(1000px)_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" aria-label="Blobby, studio mascot">
<div aria-hidden="true" class="absolute inset-0 overflow-hidden rounded-[24px] border border-white/12 bg-linear-170 from-ttc05-face1 to-ttc05-face2 shadow-[0_30px_60px_-26px_oklch(0_0_0/0.8)] after:absolute after:inset-0 after:bg-[radial-gradient(120%_60%_at_50%_0%,var(--color-ttc05-shine),transparent_60%)] after:content-['']"></div>
<div aria-hidden="true" class="absolute -top-[76px] left-1/2 h-[170px] w-[150px] -translate-x-1/2 translate-z-[45px] bg-[radial-gradient(60%_55%_at_50%_32%,var(--color-ttc05-skin2)_0_58%,transparent_59%),radial-gradient(75%_48%_at_50%_78%,var(--color-ttc05-skin)_0_60%,transparent_61%)] drop-shadow-[0_22px_18px_oklch(0_0_0/0.5)] transition-[translate,scale] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.3)] group-data-live/card:translate-z-[70px] group-data-live/card:scale-106 motion-reduce:transition-none">
<span class="absolute top-[40px] left-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[40px] right-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[74px] left-1/2 h-2.5 w-7 -translate-x-1/2 rounded-b-full bg-ttc05-ink"></span>
</div>
<div class="absolute inset-x-5 bottom-5 translate-z-[30px]">
<h3 class="text-[26px] leading-none font-bold">Blobby</h3>
<p class="mt-1.5 text-[13px] text-ttc05-mut">Chief Vibe Officer · est. 2019</p>
</div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/12 bg-white/5 px-3.5 py-2 font-ttc05-mono text-[11.5px] text-ttc05-mut">overflow-visible · translate-z-[70px] · [translate:var(--ex)]</p>
</div>
</section><section class="ttc-05 w-full min-h-svh grid place-items-center bg-ttc05-bg bg-[radial-gradient(80%_90%_at_80%_10%,var(--color-ttc05-lobe1),transparent_60%),radial-gradient(70%_80%_at_10%_90%,var(--color-ttc05-lobe2),transparent_60%)] px-6 pt-28 pb-14 font-ttc05-sans text-white" aria-label="3D pop-out character card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc05-card class="group/card relative h-64 w-full max-w-[18rem] overflow-visible rounded-[24px] transform-3d [transform:perspective(1000px)_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" aria-label="Blobby, studio mascot">
<div aria-hidden="true" class="absolute inset-0 overflow-hidden rounded-[24px] border border-white/12 bg-linear-170 from-ttc05-face1 to-ttc05-face2 shadow-[0_30px_60px_-26px_oklch(0_0_0/0.8)] after:absolute after:inset-0 after:bg-[radial-gradient(120%_60%_at_50%_0%,var(--color-ttc05-shine),transparent_60%)] after:content-['']"></div>
<div aria-hidden="true" class="absolute -top-[76px] left-1/2 h-[170px] w-[150px] -translate-x-1/2 translate-z-[45px] bg-[radial-gradient(60%_55%_at_50%_32%,var(--color-ttc05-skin2)_0_58%,transparent_59%),radial-gradient(75%_48%_at_50%_78%,var(--color-ttc05-skin)_0_60%,transparent_61%)] drop-shadow-[0_22px_18px_oklch(0_0_0/0.5)] transition-[translate,scale] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.3)] group-data-live/card:translate-z-[70px] group-data-live/card:scale-106 motion-reduce:transition-none">
<span class="absolute top-[40px] left-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[40px] right-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[74px] left-1/2 h-2.5 w-7 -translate-x-1/2 rounded-b-full bg-ttc05-ink"></span>
</div>
<div class="absolute inset-x-5 bottom-5 translate-z-[30px]">
<h3 class="text-[26px] leading-none font-bold">Blobby</h3>
<p class="mt-1.5 text-[13px] text-ttc05-mut">Chief Vibe Officer · est. 2019</p>
</div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/12 bg-white/5 px-3.5 py-2 font-ttc05-mono text-[11.5px] text-ttc05-mut">overflow-visible · translate-z-[70px] · [translate:var(--ex)]</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-ttc05-bg: #0c1618;
--color-ttc05-bg: oklch(0.16 0.02 200);
--color-ttc05-lobe1: #1f4a26b3;
--color-ttc05-lobe1: oklch(0.3 0.09 145 / 0.7);
--color-ttc05-lobe2: #1c2f4ab3;
--color-ttc05-lobe2: oklch(0.28 0.07 250 / 0.7);
--color-ttc05-face1: #1e3a26;
--color-ttc05-face1: oklch(0.28 0.06 150);
--color-ttc05-face2: #13232a;
--color-ttc05-face2: oklch(0.2 0.04 200);
--color-ttc05-shine: #3d7a4a59;
--color-ttc05-shine: oklch(0.5 0.12 150 / 0.35);
--color-ttc05-skin: #4cc062;
--color-ttc05-skin: oklch(0.72 0.19 145);
--color-ttc05-skin2: #6fd682;
--color-ttc05-skin2: oklch(0.8 0.17 145);
--color-ttc05-ink: #0f1c1f;
--color-ttc05-ink: oklch(0.2 0.02 200);
--color-ttc05-mut: #c1d0cc;
--color-ttc05-mut: oklch(0.84 0.02 180);
--font-ttc05-sans: 'Fredoka', system-ui, sans-serif;
--font-ttc05-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-ttc05-blink: ttc05-blink 5s ease-in-out infinite;
@keyframes ttc05-blink {
0%,
95%,
100% {
scale: 1 1;
}
97.5% {
scale: 1 0.1;
}
}
}
/* 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-05 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttc05-bg: #0c1618;
--color-ttc05-bg: oklch(0.16 0.02 200);
--color-ttc05-lobe1: #1f4a26b3;
--color-ttc05-lobe1: oklch(0.3 0.09 145 / 0.7);
--color-ttc05-lobe2: #1c2f4ab3;
--color-ttc05-lobe2: oklch(0.28 0.07 250 / 0.7);
--color-ttc05-face1: #1e3a26;
--color-ttc05-face1: oklch(0.28 0.06 150);
--color-ttc05-face2: #13232a;
--color-ttc05-face2: oklch(0.2 0.04 200);
--color-ttc05-shine: #3d7a4a59;
--color-ttc05-shine: oklch(0.5 0.12 150 / 0.35);
--color-ttc05-skin: #4cc062;
--color-ttc05-skin: oklch(0.72 0.19 145);
--color-ttc05-skin2: #6fd682;
--color-ttc05-skin2: oklch(0.8 0.17 145);
--color-ttc05-ink: #0f1c1f;
--color-ttc05-ink: oklch(0.2 0.02 200);
--color-ttc05-mut: #c1d0cc;
--color-ttc05-mut: oklch(0.84 0.02 180);
--font-ttc05-sans: 'Fredoka', system-ui, sans-serif;
--font-ttc05-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-ttc05-blink: ttc05-blink 5s ease-in-out infinite;
@keyframes ttc05-blink {
0%,
95%,
100% {
scale: 1 1;
}
97.5% {
scale: 1 0.1;
}
}
}
/* 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-05 {
width: 100%;
min-height: 100svh;
}(() => {
document.querySelectorAll('[data-ttc05-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 13, EYE = 3.5; // EYE: pupil travel in px
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('--ex', ((px - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.style.setProperty('--ey', ((py - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.setAttribute('data-live', ''); // lifts the mascot to translate-z-[70px]
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
['--rx', '--ry'].forEach((p) => card.style.setProperty(p, '0deg'));
['--ex', '--ey'].forEach((p) => card.style.setProperty(p, '0px'));
});
});
})();(() => {
document.querySelectorAll('[data-ttc05-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 13, EYE = 3.5; // EYE: pupil travel in px
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('--ex', ((px - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.style.setProperty('--ey', ((py - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.setAttribute('data-live', ''); // lifts the mascot to translate-z-[70px]
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
['--rx', '--ry'].forEach((p) => card.style.setProperty(p, '0deg'));
['--ex', '--ey'].forEach((p) => card.style.setProperty(p, '0px'));
});
});
})();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-ttc05-bg: #0c1618;
--color-ttc05-bg: oklch(0.16 0.02 200);
--color-ttc05-lobe1: #1f4a26b3;
--color-ttc05-lobe1: oklch(0.3 0.09 145 / 0.7);
--color-ttc05-lobe2: #1c2f4ab3;
--color-ttc05-lobe2: oklch(0.28 0.07 250 / 0.7);
--color-ttc05-face1: #1e3a26;
--color-ttc05-face1: oklch(0.28 0.06 150);
--color-ttc05-face2: #13232a;
--color-ttc05-face2: oklch(0.2 0.04 200);
--color-ttc05-shine: #3d7a4a59;
--color-ttc05-shine: oklch(0.5 0.12 150 / 0.35);
--color-ttc05-skin: #4cc062;
--color-ttc05-skin: oklch(0.72 0.19 145);
--color-ttc05-skin2: #6fd682;
--color-ttc05-skin2: oklch(0.8 0.17 145);
--color-ttc05-ink: #0f1c1f;
--color-ttc05-ink: oklch(0.2 0.02 200);
--color-ttc05-mut: #c1d0cc;
--color-ttc05-mut: oklch(0.84 0.02 180);
--font-ttc05-sans: 'Fredoka', system-ui, sans-serif;
--font-ttc05-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-ttc05-blink: ttc05-blink 5s ease-in-out infinite;
@keyframes ttc05-blink {
0%,
95%,
100% {
scale: 1 1;
}
97.5% {
scale: 1 0.1;
}
}
}
/* 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-05 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ttc05-bg: #0c1618;
--color-ttc05-bg: oklch(0.16 0.02 200);
--color-ttc05-lobe1: #1f4a26b3;
--color-ttc05-lobe1: oklch(0.3 0.09 145 / 0.7);
--color-ttc05-lobe2: #1c2f4ab3;
--color-ttc05-lobe2: oklch(0.28 0.07 250 / 0.7);
--color-ttc05-face1: #1e3a26;
--color-ttc05-face1: oklch(0.28 0.06 150);
--color-ttc05-face2: #13232a;
--color-ttc05-face2: oklch(0.2 0.04 200);
--color-ttc05-shine: #3d7a4a59;
--color-ttc05-shine: oklch(0.5 0.12 150 / 0.35);
--color-ttc05-skin: #4cc062;
--color-ttc05-skin: oklch(0.72 0.19 145);
--color-ttc05-skin2: #6fd682;
--color-ttc05-skin2: oklch(0.8 0.17 145);
--color-ttc05-ink: #0f1c1f;
--color-ttc05-ink: oklch(0.2 0.02 200);
--color-ttc05-mut: #c1d0cc;
--color-ttc05-mut: oklch(0.84 0.02 180);
--font-ttc05-sans: 'Fredoka', system-ui, sans-serif;
--font-ttc05-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-ttc05-blink: ttc05-blink 5s ease-in-out infinite;
@keyframes ttc05-blink {
0%,
95%,
100% {
scale: 1 1;
}
97.5% {
scale: 1 0.1;
}
}
}
/* 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-05 {
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=Fredoka:wght@400..700&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=Fredoka:wght@400..700&family=JetBrains+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.thc-05__face::afterbecameafter:utilities on the face div; the mascot's layered radial gradients are one arbitrarybg-[…].- The mascot's
translate:-50% 0+transform:translateZ()pair became-translate-x-1/2 translate-z-[45px]— both write the sametranslateproperty, so there is no longer a transform/translate split to reason about. - UI upgrade from the CSS original: the character now looks at you — pupils track the cursor through
--ex/--ey— and blinks every five seconds; both stop undermotion-reduce:. - UI upgrade: Fredoka replaces Segoe UI for a rounded, character-brand voice; a mouth completes the face, and the subtitle rose from 60% white to
oklch(0.84).
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 Pop-Out Character Card with Cursor-Tracking Eyes
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-pop-out-character-card-cursor-tracking-eyes/
The scroll-stopping agency trick: a mascot whose head rises OUTSIDE the card, floating at translate-z-[45px] and lifting to 70px on hover while the card tilts beneath it. The port gives the character a personality upgrade — pupils that follow the cursor and an idle blink — using the same custom-property bridge that drives the tilt.
## HTML
```html
<section class="ttc-05 w-full min-h-svh grid place-items-center bg-ttc05-bg bg-[radial-gradient(80%_90%_at_80%_10%,var(--color-ttc05-lobe1),transparent_60%),radial-gradient(70%_80%_at_10%_90%,var(--color-ttc05-lobe2),transparent_60%)] px-6 pt-28 pb-14 font-ttc05-sans text-white" aria-label="3D pop-out character card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc05-card class="group/card relative h-64 w-full max-w-[18rem] overflow-visible rounded-[24px] transform-3d [transform:perspective(1000px)_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" aria-label="Blobby, studio mascot">
<div aria-hidden="true" class="absolute inset-0 overflow-hidden rounded-[24px] border border-white/12 bg-linear-170 from-ttc05-face1 to-ttc05-face2 shadow-[0_30px_60px_-26px_oklch(0_0_0/0.8)] after:absolute after:inset-0 after:bg-[radial-gradient(120%_60%_at_50%_0%,var(--color-ttc05-shine),transparent_60%)] after:content-['']"></div>
<div aria-hidden="true" class="absolute -top-[76px] left-1/2 h-[170px] w-[150px] -translate-x-1/2 translate-z-[45px] bg-[radial-gradient(60%_55%_at_50%_32%,var(--color-ttc05-skin2)_0_58%,transparent_59%),radial-gradient(75%_48%_at_50%_78%,var(--color-ttc05-skin)_0_60%,transparent_61%)] drop-shadow-[0_22px_18px_oklch(0_0_0/0.5)] transition-[translate,scale] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.3)] group-data-live/card:translate-z-[70px] group-data-live/card:scale-106 motion-reduce:transition-none">
<span class="absolute top-[40px] left-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[40px] right-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[74px] left-1/2 h-2.5 w-7 -translate-x-1/2 rounded-b-full bg-ttc05-ink"></span>
</div>
<div class="absolute inset-x-5 bottom-5 translate-z-[30px]">
<h3 class="text-[26px] leading-none font-bold">Blobby</h3>
<p class="mt-1.5 text-[13px] text-ttc05-mut">Chief Vibe Officer · est. 2019</p>
</div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/12 bg-white/5 px-3.5 py-2 font-ttc05-mono text-[11.5px] text-ttc05-mut">overflow-visible · translate-z-[70px] · [translate:var(--ex)]</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttc05-bg: #0c1618;
--color-ttc05-bg: oklch(0.16 0.02 200);
--color-ttc05-lobe1: #1f4a26b3;
--color-ttc05-lobe1: oklch(0.3 0.09 145 / 0.7);
--color-ttc05-lobe2: #1c2f4ab3;
--color-ttc05-lobe2: oklch(0.28 0.07 250 / 0.7);
--color-ttc05-face1: #1e3a26;
--color-ttc05-face1: oklch(0.28 0.06 150);
--color-ttc05-face2: #13232a;
--color-ttc05-face2: oklch(0.2 0.04 200);
--color-ttc05-shine: #3d7a4a59;
--color-ttc05-shine: oklch(0.5 0.12 150 / 0.35);
--color-ttc05-skin: #4cc062;
--color-ttc05-skin: oklch(0.72 0.19 145);
--color-ttc05-skin2: #6fd682;
--color-ttc05-skin2: oklch(0.8 0.17 145);
--color-ttc05-ink: #0f1c1f;
--color-ttc05-ink: oklch(0.2 0.02 200);
--color-ttc05-mut: #c1d0cc;
--color-ttc05-mut: oklch(0.84 0.02 180);
--font-ttc05-sans: 'Fredoka', system-ui, sans-serif;
--font-ttc05-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-ttc05-blink: ttc05-blink 5s ease-in-out infinite;
@keyframes ttc05-blink {
0%,
95%,
100% {
scale: 1 1;
}
97.5% {
scale: 1 0.1;
}
}
}
/* 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-05 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('[data-ttc05-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 13, EYE = 3.5; // EYE: pupil travel in px
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('--ex', ((px - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.style.setProperty('--ey', ((py - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.setAttribute('data-live', ''); // lifts the mascot to translate-z-[70px]
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
['--rx', '--ry'].forEach((p) => card.style.setProperty(p, '0deg'));
['--ex', '--ey'].forEach((p) => card.style.setProperty(p, '0px'));
});
});
})();
```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 Pop-Out Character Card with Cursor-Tracking Eyes
Source: https://codefronts.com/components/tailwind-css-3d-tilt-hover-cards/tailwind-3d-pop-out-character-card-cursor-tracking-eyes/
The scroll-stopping agency trick: a mascot whose head rises OUTSIDE the card, floating at translate-z-[45px] and lifting to 70px on hover while the card tilts beneath it. The port gives the character a personality upgrade — pupils that follow the cursor and an idle blink — using the same custom-property bridge that drives the tilt.
## HTML
```html
<section class="ttc-05 w-full min-h-svh grid place-items-center bg-ttc05-bg bg-[radial-gradient(80%_90%_at_80%_10%,var(--color-ttc05-lobe1),transparent_60%),radial-gradient(70%_80%_at_10%_90%,var(--color-ttc05-lobe2),transparent_60%)] px-6 pt-28 pb-14 font-ttc05-sans text-white" aria-label="3D pop-out character card demo">
<div class="grid w-full justify-items-center gap-8">
<article data-ttc05-card class="group/card relative h-64 w-full max-w-[18rem] overflow-visible rounded-[24px] transform-3d [transform:perspective(1000px)_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" aria-label="Blobby, studio mascot">
<div aria-hidden="true" class="absolute inset-0 overflow-hidden rounded-[24px] border border-white/12 bg-linear-170 from-ttc05-face1 to-ttc05-face2 shadow-[0_30px_60px_-26px_oklch(0_0_0/0.8)] after:absolute after:inset-0 after:bg-[radial-gradient(120%_60%_at_50%_0%,var(--color-ttc05-shine),transparent_60%)] after:content-['']"></div>
<div aria-hidden="true" class="absolute -top-[76px] left-1/2 h-[170px] w-[150px] -translate-x-1/2 translate-z-[45px] bg-[radial-gradient(60%_55%_at_50%_32%,var(--color-ttc05-skin2)_0_58%,transparent_59%),radial-gradient(75%_48%_at_50%_78%,var(--color-ttc05-skin)_0_60%,transparent_61%)] drop-shadow-[0_22px_18px_oklch(0_0_0/0.5)] transition-[translate,scale] duration-600 ease-[cubic-bezier(0.2,0.9,0.3,1.3)] group-data-live/card:translate-z-[70px] group-data-live/card:scale-106 motion-reduce:transition-none">
<span class="absolute top-[40px] left-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[40px] right-[48px] grid h-6 w-[18px] origin-center place-items-center rounded-full bg-white animate-ttc05-blink motion-reduce:animate-none"><span class="size-2.5 rounded-full bg-ttc05-ink [translate:var(--ex,0px)_var(--ey,0px)] transition-[translate] duration-150"></span></span>
<span class="absolute top-[74px] left-1/2 h-2.5 w-7 -translate-x-1/2 rounded-b-full bg-ttc05-ink"></span>
</div>
<div class="absolute inset-x-5 bottom-5 translate-z-[30px]">
<h3 class="text-[26px] leading-none font-bold">Blobby</h3>
<p class="mt-1.5 text-[13px] text-ttc05-mut">Chief Vibe Officer · est. 2019</p>
</div>
</article>
<p aria-hidden="true" class="rounded-full border border-white/12 bg-white/5 px-3.5 py-2 font-ttc05-mono text-[11.5px] text-ttc05-mut">overflow-visible · translate-z-[70px] · [translate:var(--ex)]</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ttc05-bg: #0c1618;
--color-ttc05-bg: oklch(0.16 0.02 200);
--color-ttc05-lobe1: #1f4a26b3;
--color-ttc05-lobe1: oklch(0.3 0.09 145 / 0.7);
--color-ttc05-lobe2: #1c2f4ab3;
--color-ttc05-lobe2: oklch(0.28 0.07 250 / 0.7);
--color-ttc05-face1: #1e3a26;
--color-ttc05-face1: oklch(0.28 0.06 150);
--color-ttc05-face2: #13232a;
--color-ttc05-face2: oklch(0.2 0.04 200);
--color-ttc05-shine: #3d7a4a59;
--color-ttc05-shine: oklch(0.5 0.12 150 / 0.35);
--color-ttc05-skin: #4cc062;
--color-ttc05-skin: oklch(0.72 0.19 145);
--color-ttc05-skin2: #6fd682;
--color-ttc05-skin2: oklch(0.8 0.17 145);
--color-ttc05-ink: #0f1c1f;
--color-ttc05-ink: oklch(0.2 0.02 200);
--color-ttc05-mut: #c1d0cc;
--color-ttc05-mut: oklch(0.84 0.02 180);
--font-ttc05-sans: 'Fredoka', system-ui, sans-serif;
--font-ttc05-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-ttc05-blink: ttc05-blink 5s ease-in-out infinite;
@keyframes ttc05-blink {
0%,
95%,
100% {
scale: 1 1;
}
97.5% {
scale: 1 0.1;
}
}
}
/* 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-05 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('[data-ttc05-card]').forEach((card) => {
if (card.dataset.wired) return;
card.dataset.wired = '1';
if (!matchMedia('(hover:hover) and (pointer:fine)').matches) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const MAX = 13, EYE = 3.5; // EYE: pupil travel in px
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('--ex', ((px - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.style.setProperty('--ey', ((py - 0.5) * 2 * EYE).toFixed(2) + 'px');
card.setAttribute('data-live', ''); // lifts the mascot to translate-z-[70px]
});
card.addEventListener('pointerleave', () => {
card.removeAttribute('data-live');
['--rx', '--ry'].forEach((p) => card.style.setProperty(p, '0deg'));
['--ex', '--ey'].forEach((p) => card.style.setProperty(p, '0px'));
});
});
})();
```How this works
Two containers, two overflow rules. The article keeps overflow-visible so the mascot (absolute, -top-[76px]) escapes; a separate __face div carries the rounded corners and overflow-hidden for the background art. Swap those and you either clip the head or leak the art.
The eyes reuse the bridge. Alongside --rx/--ry, the handler writes --ex/--ey in pixels (±3.5px), and each pupil reads them in [translate:var(--ex,0px)_var(--ey,0px)] with a 150ms transition. On leave they return to centre with the card.
Lift on hover is group-data-live/card:translate-z-[70px] group-data-live/card:scale-106 — the individual translate and scale properties, so the mascot's own transition never fights the card's transform.
Make it yours
- Swap the CSS mascot for a transparent PNG/WebP character —
drop-shadow-[…]follows any alpha silhouette. - Raise the pop: increase
-top-[76px]and the hovertranslate-ztogether. - Give the head a lean: add
[rotate:calc(var(--ry,0deg)*-0.3)]to the mascot. - Stretch the eye range with
EYEin the JS (3.5px here).
Gotchas — read before shipping
- Reserve headroom above the card (
pt-28on the stage) or the head collides with content above. drop-shadowon big cutouts is paint-heavy; one mascot per card, not one per grid cell.- Pupils move in pixels, not degrees — keep the range under a third of the sclera width or they escape the eye.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Individual translate/scale properties are Chrome 104 / Safari 14.1 / Firefox 72; the floor is Tailwind v4's sheet.