3D Tilt Profile Card
An interactive 3D profile card with smooth mouse tilt, layered parallax depth, cursor-follow glow and dynamic glare effects.
Alex Rivera
Building clean interfaces, smooth interactions and memorable digital experiences.
<div class="cr-tilt-profile" id="crTiltProfile">
<div class="cr-tilt-profile__stage">
<article class="cr-tilt-profile__card">
<!-- CURSOR FOLLOW GLOW -->
<div
class="cr-tilt-profile__glow"
aria-hidden="true"
></div>
<!-- GLARE EFFECT -->
<div
class="cr-tilt-profile__glare"
aria-hidden="true"
></div>
<!-- DECORATIVE BACKGROUND -->
<div
class="cr-tilt-profile__decor"
aria-hidden="true"
>
<span
class="cr-tilt-profile__orb cr-tilt-profile__orb--one"
></span>
<span
class="cr-tilt-profile__orb cr-tilt-profile__orb--two"
></span>
<span
class="cr-tilt-profile__grid"
></span>
</div>
<!-- PROFILE CONTENT -->
<div class="cr-tilt-profile__content">
<!-- TOP STATUS -->
<div
class="cr-tilt-profile__top"
data-depth="18"
>
<span class="cr-tilt-profile__status">
<span
class="cr-tilt-profile__status-dot"
></span>
Available for work
</span>
<span class="cr-tilt-profile__badge">
PRO
</span>
</div>
<!-- AVATAR -->
<div
class="cr-tilt-profile__avatar-wrap"
data-depth="34"
>
<div class="cr-tilt-profile__avatar">
<span>AR</span>
</div>
<span
class="cr-tilt-profile__avatar-ring"
></span>
</div>
<!-- PROFILE INFO -->
<div
class="cr-tilt-profile__info"
data-depth="26"
>
<span class="cr-tilt-profile__eyebrow">
FRONTEND DEVELOPER
</span>
<h2>Alex Rivera</h2>
<p>
Building clean interfaces, smooth interactions
and memorable digital experiences.
</p>
</div>
<!-- SKILLS -->
<div
class="cr-tilt-profile__skills"
data-depth="22"
>
<span>HTML</span>
<span>CSS</span>
<span>JavaScript</span>
</div>
<!-- STATS -->
<div
class="cr-tilt-profile__stats"
data-depth="20"
>
<div class="cr-tilt-profile__stat">
<strong>42</strong>
<span>Projects</span>
</div>
<div
class="cr-tilt-profile__divider"
></div>
<div class="cr-tilt-profile__stat">
<strong>18K</strong>
<span>Followers</span>
</div>
<div
class="cr-tilt-profile__divider"
></div>
<div class="cr-tilt-profile__stat">
<strong>4.9</strong>
<span>Rating</span>
</div>
</div>
<!-- ACTIONS -->
<div
class="cr-tilt-profile__actions"
data-depth="30"
>
<button
class="cr-tilt-profile__button cr-tilt-profile__button--primary"
type="button"
>
<span>View Profile</span>
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
d="M5 12h14M13 6l6 6-6 6"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<button
class="cr-tilt-profile__button cr-tilt-profile__button--icon"
type="button"
aria-label="Send message"
>
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
d="M21 3L9.5 14.5M21 3l-7 18-4.5-6.5L3 10l18-7z"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</div>
<!-- BOTTOM ACCENT -->
<div
class="cr-tilt-profile__accent"
aria-hidden="true"
></div>
</article>
<!-- INTERACTION HINT -->
<div class="cr-tilt-profile__hint">
<span class="cr-tilt-profile__hint-icon">
↗
</span>
Move your cursor over the card
</div>
</div>
</div>
/* =========================================
3D TILT PROFILE CARD
========================================= */
.cr-tilt-profile {
--cr-bg: #070914;
--cr-card: rgba(13, 17, 35, 0.86);
--cr-card-border: rgba(255, 255, 255, 0.1);
--cr-cyan: #22d3ee;
--cr-cyan-soft: rgba(34, 211, 238, 0.16);
--cr-purple: #a855f7;
--cr-purple-soft: rgba(168, 85, 247, 0.16);
--cr-text: #f8fafc;
--cr-muted: #94a3b8;
--cr-soft: #cbd5e1;
width: 100%;
min-height: 650px;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 20px;
background:
radial-gradient(
circle at 25% 20%,
rgba(34, 211, 238, 0.08),
transparent 32%
),
radial-gradient(
circle at 78% 78%,
rgba(168, 85, 247, 0.1),
transparent 35%
),
var(--cr-bg);
font-family:
Inter,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
box-sizing: border-box;
overflow: hidden;
}
.cr-tilt-profile *,
.cr-tilt-profile *::before,
.cr-tilt-profile *::after {
box-sizing: border-box;
}
/* STAGE */
.cr-tilt-profile__stage {
width: min(100%, 420px);
display: flex;
flex-direction: column;
align-items: center;
perspective: 1100px;
}
/* CARD */
.cr-tilt-profile__card {
position: relative;
width: 100%;
min-height: 535px;
padding: 28px;
border: 1px solid var(--cr-card-border);
border-radius: 28px;
background:
linear-gradient(
145deg,
rgba(255, 255, 255, 0.075),
rgba(255, 255, 255, 0.018)
),
var(--cr-card);
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.02) inset,
0 0 55px rgba(34, 211, 238, 0.035);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
transform-style: preserve-3d;
transform:
rotateX(0deg)
rotateY(0deg);
transition:
transform 180ms ease,
box-shadow 250ms ease,
border-color 250ms ease;
overflow: hidden;
will-change: transform;
}
.cr-tilt-profile__card:hover {
border-color: rgba(34, 211, 238, 0.22);
box-shadow:
0 38px 95px rgba(0, 0, 0, 0.58),
0 0 0 1px rgba(255, 255, 255, 0.025) inset,
0 0 65px rgba(34, 211, 238, 0.07),
0 0 80px rgba(168, 85, 247, 0.045);
}
/* CURSOR GLOW */
.cr-tilt-profile__glow {
position: absolute;
left: 50%;
top: 50%;
width: 320px;
height: 320px;
border-radius: 50%;
background:
radial-gradient(
circle,
rgba(34, 211, 238, 0.16) 0%,
rgba(168, 85, 247, 0.08) 34%,
transparent 70%
);
transform: translate(-50%, -50%);
filter: blur(18px);
opacity: 0.5;
pointer-events: none;
z-index: 0;
transition: opacity 250ms ease;
}
.cr-tilt-profile__card:hover
.cr-tilt-profile__glow {
opacity: 0.9;
}
/* GLARE */
.cr-tilt-profile__glare {
position: absolute;
inset: 0;
background:
linear-gradient(
120deg,
transparent 20%,
rgba(255, 255, 255, 0.035) 40%,
rgba(255, 255, 255, 0.12) 50%,
rgba(255, 255, 255, 0.025) 60%,
transparent 80%
);
opacity: 0;
pointer-events: none;
mix-blend-mode: screen;
z-index: 6;
transition: opacity 250ms ease;
}
.cr-tilt-profile__card:hover
.cr-tilt-profile__glare {
opacity: 0.5;
}
/* DECORATIVE BACKGROUND */
.cr-tilt-profile__decor {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 0;
}
.cr-tilt-profile__orb {
position: absolute;
width: 190px;
height: 190px;
border-radius: 50%;
filter: blur(12px);
opacity: 0.18;
}
.cr-tilt-profile__orb--one {
top: -90px;
right: -65px;
background:
radial-gradient(
circle,
var(--cr-cyan),
transparent 68%
);
}
.cr-tilt-profile__orb--two {
bottom: -100px;
left: -75px;
background:
radial-gradient(
circle,
var(--cr-purple),
transparent 68%
);
}
.cr-tilt-profile__grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(
rgba(255, 255, 255, 0.018) 1px,
transparent 1px
),
linear-gradient(
90deg,
rgba(255, 255, 255, 0.018) 1px,
transparent 1px
);
background-size: 28px 28px;
mask-image:
linear-gradient(
to bottom,
rgba(0, 0, 0, 0.8),
transparent 80%
);
-webkit-mask-image:
linear-gradient(
to bottom,
rgba(0, 0, 0, 0.8),
transparent 80%
);
}
/* CONTENT */
.cr-tilt-profile__content {
position: relative;
z-index: 3;
display: flex;
flex-direction: column;
min-height: 475px;
transform-style: preserve-3d;
}
.cr-tilt-profile [data-depth] {
position: relative;
transform-style: preserve-3d;
will-change: transform;
transition: transform 180ms ease;
}
/* TOP ROW */
.cr-tilt-profile__top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28px;
}
.cr-tilt-profile__status {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--cr-soft);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
}
.cr-tilt-profile__status-dot {
position: relative;
width: 8px;
height: 8px;
border-radius: 50%;
background: #34d399;
box-shadow:
0 0 0 4px rgba(52, 211, 153, 0.08),
0 0 14px rgba(52, 211, 153, 0.55);
}
.cr-tilt-profile__status-dot::after {
content: "";
position: absolute;
inset: -4px;
border: 1px solid rgba(52, 211, 153, 0.25);
border-radius: inherit;
animation:
crTiltStatusPulse 2.2s ease-out infinite;
}
@keyframes crTiltStatusPulse {
0% {
transform: scale(0.7);
opacity: 0.8;
}
70%,
100% {
transform: scale(1.8);
opacity: 0;
}
}
.cr-tilt-profile__badge {
padding: 6px 10px;
border:
1px solid rgba(168, 85, 247, 0.25);
border-radius: 999px;
background:
rgba(168, 85, 247, 0.08);
color: #d8b4fe;
font-size: 9px;
font-weight: 800;
letter-spacing: 0.14em;
box-shadow:
0 0 18px rgba(168, 85, 247, 0.08);
}
/* AVATAR */
.cr-tilt-profile__avatar-wrap {
position: relative;
width: 104px;
height: 104px;
margin: 2px auto 22px;
display: flex;
align-items: center;
justify-content: center;
}
.cr-tilt-profile__avatar {
position: relative;
width: 88px;
height: 88px;
display: flex;
align-items: center;
justify-content: center;
border:
1px solid rgba(255, 255, 255, 0.12);
border-radius: 26px;
background:
linear-gradient(
135deg,
rgba(34, 211, 238, 0.22),
rgba(168, 85, 247, 0.22)
),
#11162b;
box-shadow:
0 15px 35px rgba(0, 0, 0, 0.3),
0 0 30px rgba(34, 211, 238, 0.1);
transform: rotate(-4deg);
z-index: 2;
}
.cr-tilt-profile__avatar::before {
content: "";
position: absolute;
inset: 5px;
border:
1px solid rgba(255, 255, 255, 0.08);
border-radius: 21px;
pointer-events: none;
}
.cr-tilt-profile__avatar span {
background:
linear-gradient(
135deg,
#67e8f9,
#c084fc
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-size: 27px;
font-weight: 800;
letter-spacing: -0.04em;
}
.cr-tilt-profile__avatar-ring {
position: absolute;
inset: 0;
border:
1px dashed rgba(34, 211, 238, 0.2);
border-radius: 32px;
animation:
crTiltAvatarRing 14s linear infinite;
}
@keyframes crTiltAvatarRing {
to {
transform: rotate(360deg);
}
}
/* PROFILE INFO */
.cr-tilt-profile__info {
text-align: center;
}
.cr-tilt-profile__eyebrow {
display: block;
margin-bottom: 7px;
color: var(--cr-cyan);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.16em;
}
.cr-tilt-profile__info h2 {
margin: 0;
color: var(--cr-text);
font-size: clamp(25px, 5vw, 31px);
font-weight: 750;
line-height: 1.1;
letter-spacing: -0.035em;
}
.cr-tilt-profile__info p {
max-width: 310px;
margin: 11px auto 0;
color: var(--cr-muted);
font-size: 12px;
line-height: 1.65;
}
/* SKILLS */
.cr-tilt-profile__skills {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 7px;
margin-top: 18px;
}
.cr-tilt-profile__skills span {
padding: 6px 10px;
border:
1px solid rgba(255, 255, 255, 0.07);
border-radius: 999px;
background:
rgba(255, 255, 255, 0.035);
color: #b6c2d3;
font-size: 9px;
font-weight: 650;
transition:
border-color 180ms ease,
color 180ms ease,
background 180ms ease,
transform 180ms ease;
}
.cr-tilt-profile__skills span:hover {
transform: translateY(-2px);
border-color:
rgba(34, 211, 238, 0.24);
background:
rgba(34, 211, 238, 0.07);
color: #67e8f9;
}
/* STATS */
.cr-tilt-profile__stats {
display: grid;
grid-template-columns:
1fr auto 1fr auto 1fr;
align-items: center;
margin-top: 24px;
padding: 15px 10px;
border-top:
1px solid rgba(255, 255, 255, 0.065);
border-bottom:
1px solid rgba(255, 255, 255, 0.065);
}
.cr-tilt-profile__stat {
text-align: center;
}
.cr-tilt-profile__stat strong {
display: block;
color: var(--cr-text);
font-size: 16px;
font-weight: 750;
line-height: 1.2;
}
.cr-tilt-profile__stat span {
display: block;
margin-top: 4px;
color: #748198;
font-size: 8px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.cr-tilt-profile__divider {
width: 1px;
height: 25px;
background:
linear-gradient(
to bottom,
transparent,
rgba(255, 255, 255, 0.12),
transparent
);
}
/* ACTION BUTTONS */
.cr-tilt-profile__actions {
display: grid;
grid-template-columns:
1fr 46px;
gap: 10px;
margin-top: auto;
padding-top: 24px;
}
.cr-tilt-profile__button {
appearance: none;
-webkit-appearance: none;
border: 0;
outline: none;
font: inherit;
cursor: pointer;
transition:
transform 180ms ease,
box-shadow 180ms ease,
border-color 180ms ease,
background 180ms ease;
}
.cr-tilt-profile__button--primary {
min-height: 46px;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
padding: 0 18px;
border:
1px solid rgba(34, 211, 238, 0.3);
border-radius: 13px;
background:
linear-gradient(
135deg,
rgba(34, 211, 238, 0.15),
rgba(168, 85, 247, 0.13)
);
color: #e6fbff;
font-size: 11px;
font-weight: 700;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
0 8px 22px rgba(0, 0, 0, 0.18);
}
.cr-tilt-profile__button--primary svg {
width: 16px;
height: 16px;
transition: transform 180ms ease;
}
.cr-tilt-profile__button--primary:hover {
transform: translateY(-2px);
border-color:
rgba(34, 211, 238, 0.52);
background:
linear-gradient(
135deg,
rgba(34, 211, 238, 0.22),
rgba(168, 85, 247, 0.19)
);
box-shadow:
0 12px 28px rgba(34, 211, 238, 0.09),
inset 0 1px 0 rgba(255, 255, 255, 0.09);
}
.cr-tilt-profile__button--primary:hover svg {
transform: translateX(3px);
}
.cr-tilt-profile__button--icon {
width: 46px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
border:
1px solid rgba(255, 255, 255, 0.09);
border-radius: 13px;
background:
rgba(255, 255, 255, 0.035);
color: #b9c5d6;
}
.cr-tilt-profile__button--icon svg {
width: 17px;
height: 17px;
}
.cr-tilt-profile__button--icon:hover {
transform: translateY(-2px);
border-color:
rgba(168, 85, 247, 0.32);
background:
rgba(168, 85, 247, 0.09);
color: #d8b4fe;
}
/* BOTTOM ACCENT */
.cr-tilt-profile__accent {
position: absolute;
left: 18%;
right: 18%;
bottom: -1px;
height: 2px;
background:
linear-gradient(
90deg,
transparent,
var(--cr-cyan),
var(--cr-purple),
transparent
);
box-shadow:
0 0 20px rgba(34, 211, 238, 0.28),
0 0 30px rgba(168, 85, 247, 0.2);
opacity: 0.8;
pointer-events: none;
z-index: 8;
}
/* INTERACTION HINT */
.cr-tilt-profile__hint {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
margin-top: 18px;
color: #5f6c82;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.03em;
}
.cr-tilt-profile__hint-icon {
color: var(--cr-cyan);
font-size: 13px;
animation:
crTiltHintMove 1.8s ease-in-out infinite;
}
@keyframes crTiltHintMove {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(3px, -3px);
}
}
/* KEYBOARD FOCUS */
.cr-tilt-profile__button:focus-visible {
outline: 2px solid var(--cr-cyan);
outline-offset: 3px;
}
/* MOBILE / TOUCH */
@media (max-width: 520px) {
.cr-tilt-profile {
min-height: auto;
padding: 32px 16px;
}
.cr-tilt-profile__stage {
width: 100%;
max-width: 390px;
}
.cr-tilt-profile__card {
min-height: 510px;
padding: 23px 20px;
border-radius: 24px;
}
.cr-tilt-profile__top {
margin-bottom: 23px;
}
.cr-tilt-profile__avatar-wrap {
width: 94px;
height: 94px;
margin-bottom: 19px;
}
.cr-tilt-profile__avatar {
width: 80px;
height: 80px;
border-radius: 23px;
}
.cr-tilt-profile__avatar::before {
border-radius: 18px;
}
.cr-tilt-profile__avatar span {
font-size: 24px;
}
.cr-tilt-profile__info h2 {
font-size: 27px;
}
.cr-tilt-profile__info p {
font-size: 11px;
}
.cr-tilt-profile__stats {
padding-left: 2px;
padding-right: 2px;
}
}
/* DEVICES WITHOUT HOVER */
@media (hover: none), (pointer: coarse) {
.cr-tilt-profile__hint {
display: none;
}
.cr-tilt-profile__card {
transform: none !important;
}
.cr-tilt-profile [data-depth] {
transform: none !important;
}
.cr-tilt-profile__glare {
display: none;
}
}
/* REDUCED MOTION */
@media (prefers-reduced-motion: reduce) {
.cr-tilt-profile__card,
.cr-tilt-profile [data-depth],
.cr-tilt-profile__button,
.cr-tilt-profile__button svg,
.cr-tilt-profile__skills span {
transition: none !important;
}
.cr-tilt-profile__status-dot::after,
.cr-tilt-profile__avatar-ring,
.cr-tilt-profile__hint-icon {
animation: none !important;
}
}
(() => {
const component =
document.querySelector("#crTiltProfile");
if (!component) return;
const card =
component.querySelector(
".cr-tilt-profile__card"
);
const glow =
component.querySelector(
".cr-tilt-profile__glow"
);
const glare =
component.querySelector(
".cr-tilt-profile__glare"
);
const depthItems =
component.querySelectorAll(
"[data-depth]"
);
if (!card || !glow || !glare) return;
/* SETTINGS */
const MAX_TILT = 10;
const PARALLAX_STRENGTH = 0.32;
const canHover =
window.matchMedia(
"(hover: hover) and (pointer: fine)"
);
const reduceMotion =
window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
let targetRotateX = 0;
let targetRotateY = 0;
let currentRotateX = 0;
let currentRotateY = 0;
let pointerX = 0;
let pointerY = 0;
let isHovering = false;
let animationFrame = null;
/* HELPERS */
const clamp = (value, min, max) => {
return Math.min(
Math.max(value, min),
max
);
};
/* DEPTH / PARALLAX */
const updateDepthItems = () => {
depthItems.forEach((item) => {
const depth =
Number.parseFloat(
item.dataset.depth
) || 0;
const moveX =
pointerX *
depth *
PARALLAX_STRENGTH;
const moveY =
pointerY *
depth *
PARALLAX_STRENGTH;
item.style.transform =
`translate3d(
${moveX}px,
${moveY}px,
${depth}px
)`;
});
};
/* RESET DEPTH */
const resetDepthItems = () => {
depthItems.forEach((item) => {
item.style.transform =
"translate3d(0px, 0px, 0px)";
});
};
/* SMOOTH ANIMATION */
const animate = () => {
currentRotateX +=
(targetRotateX - currentRotateX) *
0.12;
currentRotateY +=
(targetRotateY - currentRotateY) *
0.12;
card.style.transform =
`rotateX(${currentRotateX}deg)
rotateY(${currentRotateY}deg)`;
if (
isHovering ||
Math.abs(currentRotateX) > 0.01 ||
Math.abs(currentRotateY) > 0.01
) {
animationFrame =
requestAnimationFrame(animate);
} else {
currentRotateX = 0;
currentRotateY = 0;
card.style.transform =
"rotateX(0deg) rotateY(0deg)";
animationFrame = null;
}
};
/* POINTER MOVE */
const handlePointerMove = (event) => {
if (
!canHover.matches ||
reduceMotion.matches
) {
return;
}
const rect =
card.getBoundingClientRect();
const x =
event.clientX - rect.left;
const y =
event.clientY - rect.top;
const normalizedX =
clamp(
(x / rect.width) * 2 - 1,
-1,
1
);
const normalizedY =
clamp(
(y / rect.height) * 2 - 1,
-1,
1
);
pointerX = normalizedX;
pointerY = normalizedY;
targetRotateX =
normalizedY * -MAX_TILT;
targetRotateY =
normalizedX * MAX_TILT;
/* CURSOR FOLLOW GLOW */
glow.style.left = `${x}px`;
glow.style.top = `${y}px`;
/* DYNAMIC GLARE */
const glareX =
50 + normalizedX * 35;
const glareY =
50 + normalizedY * 35;
glare.style.background =
`radial-gradient(
circle at ${glareX}% ${glareY}%,
rgba(255, 255, 255, 0.20) 0%,
rgba(103, 232, 249, 0.08) 18%,
rgba(168, 85, 247, 0.04) 34%,
transparent 62%
)`;
/* LAYERED PARALLAX */
updateDepthItems();
};
/* POINTER ENTER */
const handlePointerEnter = () => {
if (
!canHover.matches ||
reduceMotion.matches
) {
return;
}
isHovering = true;
glow.style.opacity = "0.9";
glare.style.opacity = "0.55";
if (!animationFrame) {
animationFrame =
requestAnimationFrame(animate);
}
};
/* POINTER LEAVE */
const handlePointerLeave = () => {
isHovering = false;
targetRotateX = 0;
targetRotateY = 0;
pointerX = 0;
pointerY = 0;
glow.style.left = "50%";
glow.style.top = "50%";
glow.style.opacity = "0.5";
glare.style.opacity = "0";
resetDepthItems();
if (!animationFrame) {
animationFrame =
requestAnimationFrame(animate);
}
};
/* EVENT LISTENERS */
card.addEventListener(
"pointerenter",
handlePointerEnter
);
card.addEventListener(
"pointermove",
handlePointerMove
);
card.addEventListener(
"pointerleave",
handlePointerLeave
);
/* WINDOW / TAB SAFETY RESET */
window.addEventListener("blur", () => {
if (isHovering) {
handlePointerLeave();
}
});
document.addEventListener(
"visibilitychange",
() => {
if (
document.hidden &&
isHovering
) {
handlePointerLeave();
}
}
);
/* INPUT MODE CHANGE */
const handleInputModeChange = () => {
if (
!canHover.matches ||
reduceMotion.matches
) {
handlePointerLeave();
card.style.transform =
"rotateX(0deg) rotateY(0deg)";
resetDepthItems();
}
};
canHover.addEventListener(
"change",
handleInputModeChange
);
reduceMotion.addEventListener(
"change",
handleInputModeChange
);
})();
How This 3D Tilt Profile Card Works
The 3D Tilt Profile Card combines a glass-style profile layout with pointer-based tilt, layered parallax, cursor lighting, and subtle CSS animations. The outer .cr-tilt-profile__stage uses perspective: 1100px, while the card and its internal depth layers use transform-style: preserve-3d. This gives the JavaScript enough 3D space to rotate the card and move individual sections forward along the Z-axis.
The card itself is limited to 420px through its stage and has a minimum height of 535px. Its visual style comes from a semi-transparent dark background, a 28px border radius, layered box shadows, and backdrop-filter: blur(20px). Cyan and purple radial gradients appear both behind the component and inside decorative orbs, while a faint 28px grid adds texture without interfering with the profile content.
Several content groups include a data-depth attribute. The top status row uses a depth of 18, the avatar uses 34, profile information uses 26, skills use 22, stats use 20, and the action area uses 30. JavaScript reads these values and applies translate3d() transforms. Because each group has a different depth, the sections move by different amounts as the pointer travels across the card.
The tilt calculation begins with the pointer position inside the card. JavaScript converts the X and Y coordinates to values between -1 and 1, then uses MAX_TILT = 10 to limit rotation to roughly 10 degrees in each direction. Instead of immediately jumping to the target rotation, the animate() function interpolates the current rotation toward it by 0.12 on each animation frame. requestAnimationFrame() keeps this movement smooth while the card is active.
The same normalized pointer values drive the parallax effect. Each data-depth value is multiplied by PARALLAX_STRENGTH, which is set to 0.32. Higher-depth elements therefore travel farther and also receive a matching positive Z translation.
Lighting follows the pointer as well. A 320px circular glow updates its left and top positions to match the cursor. The glare layer is generated dynamically as a radial gradient, with its focal point shifting by up to 35% from the center. This makes the highlight respond to the same movement as the tilt instead of behaving like a fixed overlay.
For more detail about the 3D transform system used for the rotation and layered depth, see the MDN CSS transform documentation.
There are also CSS-only animations. The green availability indicator pulses every 2.2 seconds, the dashed avatar ring completes a rotation every 14 seconds, and the cursor hint moves diagonally on a 1.8-second loop. Skill chips and both action buttons have their own small hover translations.
The component accounts for different input and accessibility conditions. On coarse pointers or devices without hover, the tilt and depth transforms are disabled, the glare is hidden, and the interaction hint disappears. The prefers-reduced-motion media query removes transitions and decorative animations for users who request reduced motion.
Customizing the 3D Tilt Profile Card
Most of the appearance can be adjusted through the CSS variables and existing sizing values, while the strength of the interactive movement is controlled by two JavaScript constants.
- Change
--cr-cyanand--cr-purpleto create a different accent palette. - Adjust
--cr-bg,--cr-card,--cr-text, and--cr-mutedto customize the dark theme and typography colors. - Change the card’s
28pxborder radius or28pxpadding to alter its shape and spacing. - Modify
MAX_TILT = 10to increase or reduce the maximum pointer-driven rotation. - Adjust
PARALLAX_STRENGTH = 0.32to control how far the internal depth layers move. - Edit the individual
data-depthvalues to change how strongly the status, avatar, profile information, skills, stats, and buttons project forward. - Change the 320px glow dimensions or its radial-gradient colors to modify the cursor-follow lighting.
- Replace the profile name, initials, role, description, skills, statistics, status, and button labels directly in the HTML.
Where to Use This 3D Tilt Profile Card
This component works best where a compact profile needs more visual interaction than a standard static card while still keeping the important information readable.
- Developer and designer portfolio profile sections
- Freelancer introduction cards
- Creative team member showcases
- Personal landing pages
- Community or creator profiles
- Dashboard user summaries
- Agency team sections
- Interactive profile previews
For layouts that need several interactive cards rather than one detailed profile, the 3D Hover Monster Cards component provides another related card-based interaction.
Because the JavaScript checks hover capability and reduced-motion preferences before applying its pointer effects, the card can retain a simpler static presentation where its full 3D interaction is not appropriate.
Frequently Asked Questions
How does the card create the 3D tilt effect?
JavaScript tracks pointermove events, converts the cursor coordinates into normalized values from -1 to 1, and maps them to rotateX() and rotateY(). The rotation is capped by the MAX_TILT value of 10 degrees and smoothly interpolated with requestAnimationFrame().
What are the data-depth attributes used for?
They assign different depth values to content groups. JavaScript uses each value with PARALLAX_STRENGTH to calculate X and Y movement and passes the depth itself to the Z position of translate3d(). This creates layered parallax inside the tilted card.
Does the 3D effect run on touch devices?
No. The script checks (hover: hover) and (pointer: fine), while the CSS also disables card and depth transforms for coarse pointers or devices without hover. The glare and cursor instruction are removed in that mode as well.
Does the component support reduced-motion preferences?
Yes. The CSS prefers-reduced-motion: reduce rule disables transitions and the status, avatar-ring, and hint animations. JavaScript also checks the same preference and avoids running the interactive pointer tilt when reduced motion is requested.
The 3D Tilt Profile Card uses CSS depth styling and pointer-aware JavaScript together, while keeping the underlying profile structure straightforward to edit for different people, roles, skills, and statistics.
