043D tilt card
The card tilts in 3D based on where the cursor sits over it, with a diagonal sheen sweeping alongside.
Best forSaaS pricing cards, product showcase cards
perspective + rotateX/rotateYbackground-position 스윕신뢰감 있는 밝은 톤

<main class="stage">
<div class="tilt-wrap">
<div class="tilt-card is-demo">
<span class="tilt-card__badge">
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="m229.06 108.79l-48.7 42l14.88 62.79a8.4 8.4 0 0 1-12.52 9.17L128 189.09l-54.72 33.65a8.4 8.4 0 0 1-12.52-9.17l14.88-62.79l-48.7-42A8.46 8.46 0 0 1 31.73 94l63.91-5.2l24.62-59.6a8.36 8.36 0 0 1 15.48 0l24.62 59.6l63.91 5.2a8.46 8.46 0 0 1 4.79 14.79" opacity=".2"/><path d="M239.18 97.26A16.38 16.38 0 0 0 224.92 86l-59-4.76l-22.78-55.09a16.36 16.36 0 0 0-30.27 0L90.11 81.23L31.08 86a16.46 16.46 0 0 0-9.37 28.86l45 38.83L53 211.75a16.38 16.38 0 0 0 24.5 17.82l50.5-31.08l50.53 31.08A16.4 16.4 0 0 0 203 211.75l-13.76-58.07l45-38.83a16.43 16.43 0 0 0 4.94-17.59m-15.34 5.47l-48.7 42a8 8 0 0 0-2.56 7.91l14.88 62.8a.37.37 0 0 1-.17.48c-.18.14-.23.11-.38 0l-54.72-33.65a8 8 0 0 0-8.38 0l-54.72 33.67c-.15.09-.19.12-.38 0a.37.37 0 0 1-.17-.48l14.88-62.8a8 8 0 0 0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16a8 8 0 0 0 6.72-4.94l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153 91.86a8 8 0 0 0 6.75 4.92l63.92 5.16c.15 0 .24 0 .33.29s0 .4-.16.5"/></g></svg>
</span>
<p class="tilt-card__title">Pro plan</p>
<p class="tilt-card__desc">The plan your whole team uses</p>
<span class="tilt-card__sheen" aria-hidden="true"></span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-instant; // 기울기 반응 시간
$easing: $ease-out;
$color: $subject-blue;
@include stage(#eef3fb);
.tilt-wrap {
width: min(200px, 62vw);
height: 130px;
display: grid;
place-items: center;
}
.tilt-card {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
padding: $sp-6 $sp-5;
border-radius: $r-card;
background: $stage-paper;
border: 1px solid rgba(47, 109, 246, .14);
box-shadow: 0 10px 26px rgba(47, 109, 246, .16);
transform: perspective(600px) rotateX(0) rotateY(0);
transition: transform $duration $easing;
will-change: transform;
}
.tilt-card__badge {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: $r-sm;
background: rgba(47, 109, 246, .12);
color: $color;
margin-bottom: $sp-3;
}
.tilt-card__badge svg { width: 18px; height: 18px; }
.tilt-card__title { margin: 0 0 $sp-1; font-size: 16px; font-weight: 800; color: #16213a; }
.tilt-card__desc { margin: 0; font-size: 12.5px; color: #5b6b8c; }
.tilt-card__sheen {
position: absolute;
inset: -40% -60%;
background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, .55) 50%, transparent 65%);
background-size: 220% 220%;
background-position: 0% 50%;
pointer-events: none;
}
@media (hover: none), (pointer: coarse) {
.tilt-card { transition: none; } // 터치엔 기울기를 줄 좌표가 없다 — 평평한 카드로 둔다
}
// 데모 강제 루프 — 포인터가 카드 네 모서리를 도는 것을 흉내
.tilt-card.is-demo { animation: tilt-loop $dur-loop $easing infinite; transition: none; }
.tilt-card.is-demo .tilt-card__sheen { animation: tilt-sheen $dur-loop $easing infinite; }
@keyframes tilt-loop {
0%, 8% { transform: perspective(600px) rotateX(0) rotateY(0); }
30% { transform: perspective(600px) rotateX(6deg) rotateY(-6deg); }
55% { transform: perspective(600px) rotateX(-6deg) rotateY(6deg); }
80%, 100% { transform: perspective(600px) rotateX(0) rotateY(0); }
}
@keyframes tilt-sheen {
0%, 8% { background-position: 10% 50%; }
30% { background-position: 70% 20%; }
55% { background-position: 30% 80%; }
80%, 100% { background-position: 10% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.tilt-card, .tilt-card__sheen { animation: none !important; transition: none !important; transform: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #eef3fb;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tilt-wrap {
width: min(200px, 62vw);
height: 130px;
display: grid;
place-items: center;
}
.tilt-card {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
padding: 24px 20px;
border-radius: 16px;
background: #ffffff;
border: 1px solid rgba(47, 109, 246, 0.14);
box-shadow: 0 10px 26px rgba(47, 109, 246, 0.16);
transform: perspective(600px) rotateX(0) rotateY(0);
transition: transform 140ms ease-out;
will-change: transform;
}
.tilt-card__badge {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 8px;
background: rgba(47, 109, 246, 0.12);
color: #2f6df6;
margin-bottom: 12px;
}
.tilt-card__badge svg {
width: 18px;
height: 18px;
}
.tilt-card__title {
margin: 0 0 4px;
font-size: 16px;
font-weight: 800;
color: #16213a;
}
.tilt-card__desc {
margin: 0;
font-size: 12.5px;
color: #5b6b8c;
}
.tilt-card__sheen {
position: absolute;
inset: -40% -60%;
background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, 0.55) 50%, transparent 65%);
background-size: 220% 220%;
background-position: 0% 50%;
pointer-events: none;
}
@media (hover: none), (pointer: coarse) {
.tilt-card {
transition: none;
}
}
.tilt-card.is-demo {
animation: tilt-loop 2s ease-out infinite;
transition: none;
}
.tilt-card.is-demo .tilt-card__sheen {
animation: tilt-sheen 2s ease-out infinite;
}
@keyframes tilt-loop {
0%, 8% {
transform: perspective(600px) rotateX(0) rotateY(0);
}
30% {
transform: perspective(600px) rotateX(6deg) rotateY(-6deg);
}
55% {
transform: perspective(600px) rotateX(-6deg) rotateY(6deg);
}
80%, 100% {
transform: perspective(600px) rotateX(0) rotateY(0);
}
}
@keyframes tilt-sheen {
0%, 8% {
background-position: 10% 50%;
}
30% {
background-position: 70% 20%;
}
55% {
background-position: 30% 80%;
}
80%, 100% {
background-position: 10% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.tilt-card, .tilt-card__sheen {
animation: none !important;
transition: none !important;
transform: none;
}
}(function () {
var wrap = document.querySelector('.tilt-wrap');
var card = wrap.querySelector('.tilt-card');
var MAX = 6; // 최대 기울기(deg)
wrap.addEventListener('pointermove', function (e) {
if (card.classList.contains('is-demo')) return;
var r = wrap.getBoundingClientRect();
var px = (e.clientX - r.left) / r.width; // 0~1
var py = (e.clientY - r.top) / r.height;
var rx = (0.5 - py) * MAX * 2;
var ry = (px - 0.5) * MAX * 2;
card.style.transform = 'perspective(600px) rotateX(' + rx + 'deg) rotateY(' + ry + 'deg)';
});
wrap.addEventListener('pointerleave', function () { card.style.transform = ''; });
})();import { useRef, type CSSProperties, type PointerEvent } from "react";
import styles from "./CursorTilt3dCard.module.scss";
export type CursorTilt3dCardProps = {
title?: string;
desc?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const MAX_DEG = 6;
/** offsetX/Y 비율로 rotateX/rotateY 를 계산해 perspective 카드에 적용한다 */
export default function CursorTilt3dCard({
title = "Pro 플랜",
desc = "팀 전체가 쓰는 요금제",
duration = 140,
easing = "ease-out",
color = "#2f6df6",
}: CursorTilt3dCardProps) {
const cardRef = useRef<HTMLDivElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const move = (e: PointerEvent<HTMLDivElement>) => {
const card = cardRef.current;
if (!card) return;
const r = e.currentTarget.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width;
const py = (e.clientY - r.top) / r.height;
const rx = (0.5 - py) * MAX_DEG * 2;
const ry = (px - 0.5) * MAX_DEG * 2;
card.style.transform = `perspective(600px) rotateX(${rx}deg) rotateY(${ry}deg)`;
};
const leave = () => { if (cardRef.current) cardRef.current.style.transform = ""; };
return (
<div className={styles.wrap} style={vars} onPointerMove={move} onPointerLeave={leave}>
<div ref={cardRef} className={styles.card}>
<span className={styles.badge}>
<svg viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="m229.06 108.79l-48.7 42l14.88 62.79a8.4 8.4 0 0 1-12.52 9.17L128 189.09l-54.72 33.65a8.4 8.4 0 0 1-12.52-9.17l14.88-62.79l-48.7-42A8.46 8.46 0 0 1 31.73 94l63.91-5.2l24.62-59.6a8.36 8.36 0 0 1 15.48 0l24.62 59.6l63.91 5.2a8.46 8.46 0 0 1 4.79 14.79" opacity=".2" /><path d="M239.18 97.26A16.38 16.38 0 0 0 224.92 86l-59-4.76l-22.78-55.09a16.36 16.36 0 0 0-30.27 0L90.11 81.23L31.08 86a16.46 16.46 0 0 0-9.37 28.86l45 38.83L53 211.75a16.38 16.38 0 0 0 24.5 17.82l50.5-31.08l50.53 31.08A16.4 16.4 0 0 0 203 211.75l-13.76-58.07l45-38.83a16.43 16.43 0 0 0 4.94-17.59m-15.34 5.47l-48.7 42a8 8 0 0 0-2.56 7.91l14.88 62.8a.37.37 0 0 1-.17.48c-.18.14-.23.11-.38 0l-54.72-33.65a8 8 0 0 0-8.38 0l-54.72 33.67c-.15.09-.19.12-.38 0a.37.37 0 0 1-.17-.48l14.88-62.8a8 8 0 0 0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16a8 8 0 0 0 6.72-4.94l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153 91.86a8 8 0 0 0 6.75 4.92l63.92 5.16c.15 0 .24 0 .33.29s0 .4-.16.5" /></g></svg>
</span>
<p className={styles.title}>{title}</p>
<p className={styles.desc}>{desc}</p>
<span className={styles.sheen} aria-hidden="true" />
</div>
</div>
);
}04 3D tilt card — Measured area 3.769% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · 9 Mouse Effects CSS Tricks You Can Copy-Paste
The post this demo lives in
9 Mouse Effects CSS Tricks You Can Copy-PasteMouse effects css tricks are small interactions that react to pointer moves, clicks, drags, or scrolls. The cursor barel See all demos in this post


