Hover the card to tilt it
3D Tilt Effect
Real-time two-axis 3D tilt tracking the cursor position — content floats forward and an indigo glow appears on hover.
This effect is free — the Cards category contains 41 effects total, including 6 free. Effect.Labs has 811 vanilla effects. Explore the category →
Usage examples
New release
Hover the card to tilt it
Hover the card to tilt it
How it works
On every mousemove, the script computes the cursor position relative to the card's center via getBoundingClientRect(). Two angles are derived: rotateX = (y − centerY) / 10 (forward/backward tilt) and rotateY = (centerX − x) / 10 (left/right tilt). The divisor 10 caps the tilt at roughly ±9° at the card's edge.
The transformation is applied as a single inline string: perspective(1000px) rotateX(Xdeg) rotateY(Ydeg). Using perspective() inside the transform function rather than as a parent CSS property binds the vanishing point to the element itself — each card in a grid has its own perspective without interference.
The CSS property transform-style: preserve-3d on .card-tilt propagates the 3D context to children. The .tilt-content element gets translateZ(30px), visually floating it 30 px in front of the card surface — a subtle parallax between the card background and its content.
On mouseleave, the transform resets to perspective(1000px) rotateX(0) rotateY(0) through the CSS transition: transform 0.1s — the snap-back is smooth in 100 ms. The indigo drop shadow from .card-tilt:hover follows the same native CSS mechanism without extra JS.
Accessibility
- prefers-reduced-motion not handled: the tilt always fires on hover regardless of the system preference. Add
@media (prefers-reduced-motion: reduce) { .card-tilt { transition: none; } }and disable the JS listeners if needed. - The effect is mouse-only (
mousemove/mouseleave) — no keyboard or touch interaction. The card stays accessible and readable without interaction. - No ARIA attributes are added by the effect. If the card is interactive (link, button), add
role,aria-label, oraria-labelledbymanually. - Text content contrast is unaffected by the tilt: colors and brightness remain constant regardless of rotation angle.
- The effect intercepts no keyboard events and does not trap focus — interactive child elements (links, buttons) remain normally focusable.
Browser compatibility
Requires CSS transform-style: preserve-3d and getBoundingClientRect — available in all modern browsers. Zero external dependencies.
Without 3D CSS transform support, the card displays flat with no tilt — no JS errors, content stays readable and accessible.
The code
Copy the three blocks into your page. No dependencies.
<div class="card-demo card-tilt" id="tiltCard">
<span class="tilt-content">3D Tilt</span>
</div>
.card-demo {
width: 280px;
height: 180px;
background: var(--bg-card);
border-radius: var(--radius-xl);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
position: relative;
cursor: pointer;
}
.card-tilt {
transform-style: preserve-3d;
transition: transform 0.1s;
}
.card-tilt:hover {
box-shadow: rgba(99, 102, 241, 0.3) 0px 25px 50px;
}
.card-tilt .tilt-content {
transform: translateZ(30px);
}
/* Variables du design system du site, dont ce code depend. Sans elles,
l'acheteur obtenait des coins carres et des couleurs par defaut (revue Owen).
Valeurs reprises de css/style.css — a adapter librement. */
.card-demo, .card-tilt {
--bg-card: #12121a;
--radius-xl: 1rem;
}
/* Le fond de carte est desormais sombre : sans couleur de texte declaree,
le libelle heritait du noir de la page hote et devenait illisible (revue Owen). */
.card-demo { color: #fff; }
(function(){
// 3D Tilt Effect
const tiltCard = document.getElementById('tiltCard');
if (tiltCard) {
tiltCard.addEventListener('mousemove', (e) => {
const rect = tiltCard.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 10;
const rotateY = (centerX - x) / 10;
tiltCard.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
});
tiltCard.addEventListener('mouseleave', () => {
tiltCard.style.transform = 'perspective(1000px) rotateX(0) rotateY(0)';
});
}
// Spotlight Effect
const spotlightCard = document.getElementById('spotlightCard');
if (spotlightCard) {
spotlightCard.addEventListener('mousemove', (e) => {
const rect = spotlightCard.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
spotlightCard.style.setProperty('--spotlight-x', x + 'px');
spotlightCard.style.setProperty('--spotlight-y', y + 'px');
});
}
// Card Morph Expand Effect
const morphExpandCard = document.getElementById('morphExpandCard');
if (morphExpandCard) {
morphExpandCard.addEventListener('click', () => {
morphExpandCard.classList.toggle('expanded');
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (!morphExpandCard.contains(e.target) && morphExpandCard.classList.contains('expanded')) {
morphExpandCard.classList.remove('expanded');
}
});
}
})();
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| Divisor /10 (JS — rotateX / rotateY lines) | 10 | Maximum tilt amplitude. Increase to soften (15 → ~6°), decrease to intensify (6 → ~15°). |
| perspective(1000px) (JS — transform string) | 1000px | Virtual camera distance. Higher = flatter perspective; lower = more pronounced 3D distortion. |
| transition: transform 0.1s (CSS — .card-tilt) | 0.1s | Cursor-follow speed. 0.05s = snappy, 0.25s = slow and languid. |
| translateZ(30px) (CSS — .card-tilt .tilt-content) | 30px | Floating depth of the content above the card surface. Increase to 60px for a holographic feel. |
| box-shadow (CSS — .card-tilt:hover) | rgba(99,102,241,0.3) 0px 25px 50px | Color and intensity of the hover drop shadow. Replace the color with your brand primary. |
| --bg-card (CSS variable) | globally defined | Card background color or gradient — redefine the variable on the card or its parent to customize. |
| width / height (CSS — .card-demo) | 280px / 180px | Card dimensions. The rotation formula uses getBoundingClientRect — resizing the card does not break the effect. |
FAQ
Does the effect work on mobile (touch)?
No — the code listens to mousemove and mouseleave, which do not fire on touch. On mobile the card displays flat. To add touch support, listen to touchmove and derive rotation from e.touches[0].clientX/Y the same way.
How do I apply the tilt to multiple cards on the same page?
The provided code targets a single element by id. For multiple instances, select by class: document.querySelectorAll('.card-tilt'), then attach mousemove and mouseleave listeners to each element in a loop — each card computes its own coordinates via getBoundingClientRect().
Do child elements (buttons, links) stay clickable inside a tilted card?
Yes. The 3D transform is applied via element.style.transform — it does not intercept clicks or DOM events from children. A button or link inside the card remains fully interactive and keyboard-focusable.