CSS 3D from Scratch · Part 3 — Interactive Tilt & Parallax
Make 3D respond to the human: a card that tilts toward the cursor in real time, layered parallax depth with translateZ, a moving glare highlight, and the will-change performance trick. Beginner-friendly, with a live tilt demo.
So far, padawan, your 3D has been admired from a distance — a flip here, a cube there. {Tới giờ, đồ đệ, 3D của con mới chỉ được ngắm từ xa — một cú flip ở đây, một cube ở kia.} Today it reaches out and touches the user back. {Hôm nay nó vươn ra và chạm lại người dùng.} We build the effect you’ve seen on every fancy product page: a card that leans toward your cursor like it’s alive, with inner layers floating at different depths. {Ta dựng hiệu ứng con đã thấy trên mọi trang sản phẩm sang chảnh: một tấm card nghiêng về phía con trỏ như có sự sống, với các lớp bên trong lơ lửng ở những độ sâu khác nhau.}
The master will be honest: this one needs a pinch of JavaScript. {Sư phụ nói thật: cái này cần một nhúm JavaScript.} CSS can’t read your mouse position. {CSS không đọc được vị trí chuột.} But the JS is tiny — maybe 12 lines — and CSS still does all the heavy 3D lifting. {Nhưng JS rất nhỏ — chừng 12 dòng — và CSS vẫn gánh toàn bộ phần 3D nặng nhọc.}
Move your mouse over the card. Then tune max tilt, perspective, and layer depth. {Di chuột lên card. Rồi chỉnh max tilt, perspective, và layer depth.}
Open the full demo {Mở demo đầy đủ}: /tools/css-3d-tilt-demo/.
The recipe {Công thức}
Every mouse-tilt card is the same three steps: {Mọi card nghiêng theo chuột đều là ba bước:}
- Put
perspectiveon the container (the camera — Part 1). {Đặtperspectivelên container (máy ảnh — Phần 1).} - In JS, read where the cursor is inside the card (a 0→1 ratio). {Trong JS, đọc vị trí con trỏ bên trong card (tỉ lệ 0→1).}
- Map that ratio to
rotateX/rotateYand write it to the card’stransform. {Ánh xạ tỉ lệ đó sangrotateX/rotateYvà ghi vàotransformcủa card.}
<div class="stage"> <!-- the camera -->
<div class="card"> <!-- the thing that tilts -->
<div class="layer">…</div>
</div>
</div>
.stage { perspective: 800px; }
.card {
transform-style: preserve-3d; /* so inner layers get real depth */
transition: transform 0.12s ease-out; /* smooth, and snaps back on leave */
}
The 12 lines of JavaScript {12 dòng JavaScript}
Here’s the entire brain of the effect. {Đây là toàn bộ bộ não của hiệu ứng.} Read the comments — the math is just “where is the mouse, from 0 to 1?” {Đọc comment — toán chỉ là “chuột đang ở đâu, từ 0 đến 1?”}
const stage = document.querySelector('.stage');
const card = document.querySelector('.card');
const MAX = 14; // degrees of tilt at the edges
stage.addEventListener('pointermove', (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width; // 0 (left) → 1 (right)
const py = (e.clientY - r.top) / r.height; // 0 (top) → 1 (bottom)
const ry = (px - 0.5) * 2 * MAX; // -MAX → +MAX as cursor goes left→right
const rx = -(py - 0.5) * 2 * MAX; // +MAX → -MAX as cursor goes top→bottom
card.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
});
stage.addEventListener('pointerleave', () => {
card.style.transform = 'rotateX(0deg) rotateY(0deg)'; // snap back flat
});
Two subtleties the master wants you to notice: {Hai điểm tinh tế sư phụ muốn con để ý:}
- We subtract
0.5so the center is neutral (0°) and the edges are full tilt. {Ta trừ0.5để tâm là trung tính (0°) còn các cạnh là nghiêng tối đa.} rotateXis negated. When the cursor is at the bottom, you want the bottom edge to come toward you — which is a negativerotateX. Flip this sign and the card tilts the “wrong” way (which, sometimes, looks cool too — try the direction toggle). {rotateXbị đảo dấu. Khi con trỏ ở dưới, con muốn cạnh dưới tiến về phía con — đó làrotateXâm. Đảo dấu này thì card nghiêng “ngược” (đôi khi cũng đẹp — thử nút direction).}
We use pointermove (not mousemove) so it works for mouse, pen, and touch with one handler. {Ta dùng pointermove (không phải mousemove) để chạy cho chuột, bút, và cảm ứng với một handler.}
Parallax: the part that sells it {Parallax: phần làm nên đẳng cấp}
A flat card that tilts is fine. {Một card phẳng mà nghiêng thì tạm ổn.} A card whose insides move at different speeds is magic. {Một card mà bên trong di chuyển ở tốc độ khác nhau mới là phép thuật.} That’s parallax, and you already have everything you need: translateZ. {Đó là parallax, và con đã có đủ mọi thứ cần: translateZ.}
Give each inner layer a different Z height and let the parent tilt carry them: {Cho mỗi lớp bên trong một độ cao Z khác nhau và để cha nghiêng mang chúng theo:}
.card { transform-style: preserve-3d; } /* required, or layers flatten */
.layer.badge { transform: translateZ(70px); } /* floats high — moves a LOT */
.layer.title { transform: translateZ(40px); }
.layer.chip { transform: translateZ(55px); }
.layer.foot { transform: translateZ(20px); } /* near the surface — barely moves */
Why does this create parallax for free? {Vì sao nó tạo parallax miễn phí?} Because when the parent rotates, a point sitting 70px above the surface swings through a much bigger arc than a point at 20px. {Vì khi cha xoay, một điểm nằm cao 70px so với bề mặt quét qua một cung lớn hơn nhiều so với điểm ở 20px.} Higher layers travel farther on screen — exactly how near and far objects behave in real life. {Lớp cao hơn di chuyển xa hơn trên màn hình — đúng như cách vật gần và xa hành xử ngoài đời.}
Drag layer depth × in the demo from 1 down to 0. {Kéo layer depth × trong demo từ 1 xuống 0.} At 0, every layer flattens to the surface and the card goes lifeless. {Ở 0, mọi lớp dẹp về bề mặt và card trở nên vô hồn.} That single slider is the difference between “nice” and “whoa”. {Cái slider đó chính là khác biệt giữa “đẹp” và “wow”.}
The glare — fake light, real polish {Glare — ánh sáng giả, độ bóng thật}
A moving highlight that follows the cursor tricks the eye into seeing a glossy surface catching light. {Một vệt sáng di chuyển theo con trỏ đánh lừa mắt thấy một bề mặt bóng đang bắt sáng.} It’s just a radial gradient whose center we move with a CSS variable: {Nó chỉ là một radial gradient mà ta dời tâm bằng một biến CSS:}
.glare {
position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(
circle at var(--gx, 50%) var(--gy, 50%),
rgba(255,255,255,0.28), transparent 45%
);
transform: translateZ(80px); /* sit it above the other layers */
}
glare.style.setProperty('--gx', `${px * 100}%`);
glare.style.setProperty('--gy', `${py * 100}%`);
We’ll do proper directional shading in Part 5. {Ta sẽ làm shading theo hướng đúng nghĩa ở Phần 5.} This is the cheap, delightful version. {Đây là bản rẻ tiền mà vui mắt.}
will-change — the performance trick {will-change — mẹo hiệu năng}
When something tilts continuously, hint the browser to promote it to its own GPU layer ahead of time: {Khi một thứ nghiêng liên tục, hãy gợi ý trình duyệt nâng nó lên một lớp GPU riêng trước:}
.card { will-change: transform; }
This avoids a hitch on the first move. {Cái này tránh giật ở lần di chuyển đầu.} But heed the master’s warning — will-change is a scalpel, not a seasoning: {Nhưng nghe lời cảnh báo của sư phụ — will-change là dao mổ, không phải gia vị:}
Don’t slap
will-changeon everything. Each one costs GPU memory. Use it only on elements that are actually about to animate, and ideally add/remove it around the interaction. {Đừng phếtwill-changelên mọi thứ. Mỗi cái tốn bộ nhớ GPU. Chỉ dùng trên phần tử thực sự sắp animate, và lý tưởng là thêm/bỏ nó quanh tương tác.}
The master’s warnings {Lời cảnh báo của sư phụ}
- Forgot
preserve-3don the card? Your parallax layers will flatten and only the tilt remains. {Quênpreserve-3dtrên card? Các lớp parallax sẽ dẹp phẳng và chỉ còn lại cái nghiêng.} - Reading the wrong rect. Measure the card’s
getBoundingClientRect(), not the window — otherwise the neutral center won’t line up with the card. {Đọc sai rect. ĐogetBoundingClientRect()của card, không phải cửa sổ — nếu không tâm trung tính sẽ lệch khỏi card.} - No reset on leave. Without the
pointerleavereset, the card freezes mid-tilt when the cursor leaves. Always snap back. {Không reset khi rời. Không có resetpointerleave, card đứng hình giữa chừng khi con trỏ rời đi. Luôn bật về phẳng.} - Respect reduced motion. Some users get dizzy. Wrap the tilt in a check (see Part 5) and simply skip it for them. {Tôn trọng reduced motion. Một số người bị chóng mặt. Bọc cái nghiêng trong một kiểm tra (xem Phần 5) và đơn giản là bỏ qua cho họ.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Build your own tilt card {Tự dựng card nghiêng}: start from a plain
<div>with a title and a button, add the 12 lines, and get it leaning toward your cursor. {bắt đầu từ một<div>đơn giản có tiêu đề và nút, thêm 12 dòng, và làm nó nghiêng về con trỏ.} - Add three parallax layers {Thêm ba lớp parallax}: put an icon, a title, and a footer at
translateZof 80, 40, and 15px. Feel the depth appear. {đặt một icon, tiêu đề, và footer ởtranslateZ80, 40, và 15px. Cảm nhận chiều sâu hiện ra.} - Smooth it out {Làm mượt}: instead of writing the transform directly, lerp toward the target each frame with
requestAnimationFramefor a buttery, weighted feel. {thay vì ghi transform trực tiếp, hãy nội suy (lerp) về mục tiêu mỗi khung hình bằngrequestAnimationFramecho cảm giác mượt như bơ, có quán tính.} - A whole grid {Cả một lưới}: make a 3×3 grid of tilt cards. (Lesson: give each its own
perspectivecontainer, or share one for a unified scene — try both.) {làm lưới 3×3 card nghiêng. (Bài học: cho mỗi cái một containerperspectiveriêng, hoặc chung một cái cho khung cảnh thống nhất — thử cả hai.)}
What’s next {Phần tiếp theo}
You can now make 3D respond to a human in real time, with convincing layered depth and a moving highlight. {Giờ con làm được 3D phản hồi con người theo thời gian thực, với chiều sâu phân lớp thuyết phục và một vệt sáng di động.} That’s the technique behind countless “premium” UIs. {Đó là kỹ thuật đứng sau vô số UI “cao cấp”.}
In Part 4, we set objects in motion on their own — pure-CSS 3D animations: a spinning showcase, and the crown jewel, a 3D carousel / coverflow built from nothing but rotateY and translateZ. {Ở Phần 4, ta cho vật thể tự chuyển động — animation 3D thuần CSS: một showcase xoay tròn, và viên ngọc quý, một carousel / coverflow 3D dựng từ đúng rotateY và translateZ.}