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.
Tới giờ, đồ đệ, 3D của con mới chỉ được ngắm từ xa — một cú flip ở đây, một cube ở kia. Hôm nay nó vươn ra và chạm lại người dùng. 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.
Sư phụ nói thật: cái này cần một nhúm JavaScript. CSS không đọc được vị trí chuột. 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.
Di chuột lên card. Rồi chỉnh max tilt, perspective, và layer depth.
Mở demo đầy đủ:
Công thức
Mọi card nghiêng theo chuột đều là ba bước:
- Đặt
perspectivelên container (máy ảnh — Phần 1). - Trong JS, đọc vị trí con trỏ bên trong card (tỉ lệ 0→1).
- Ánh xạ tỉ lệ đó sang
rotateX/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 */
}
12 dòng JavaScript
Đây là toàn bộ bộ não của hiệu ứng. Đọ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
});
Hai điểm tinh tế sư phụ muốn con để ý:
- Ta trừ
0.5để tâm là trung tính (0°) còn các cạnh là nghiêng tối đa. 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).
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: phần làm nên đẳng cấp
Một card phẳng mà nghiêng thì tạm ổn. Một card mà bên trong di chuyển ở tốc độ khác nhau mới là phép thuật. Đó là parallax, và con đã có đủ mọi thứ cần: translateZ.
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 */
Vì sao nó tạo parallax miễn phí? 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. 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.
Kéo layer depth × trong demo từ 1 xuống 0. Ở 0, mọi lớp dẹp về bề mặt và card trở nên vô hồn. Cái slider đó chính là khác biệt giữa “đẹp” và “wow”.
Glare — ánh sáng giả, độ bóng thật
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. 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}%`);
Ta sẽ làm shading theo hướng đúng nghĩa ở Phần 5. Đây là bản rẻ tiền mà vui mắt.
will-change — mẹo hiệu năng
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; }
Cái này tránh giật ở lần di chuyển đầu. Nhưng nghe lời cảnh báo của sư phụ — will-change là dao mổ, không phải gia vị:
Đừng phết
will-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.
Lời cảnh báo của sư phụ
- Quên
preserve-3dtrên card? Các lớp parallax sẽ dẹp phẳng và chỉ còn lại cái nghiêng. - Đọc sai rect. Đo
getBoundingClientRect()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. - Không reset khi rời. Không có reset
pointerleave, card đứng hình giữa chừng khi con trỏ rời đi. Luôn bật về phẳng. - 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ọ.
Luyện tập, không thì coi như chưa học
- Tự dựng card nghiêng: 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ỏ. - Thêm ba lớp parallax: đặt một icon, tiêu đề, và footer ở
translateZ80, 40, và 15px. Cảm nhận chiều sâu hiện ra. - Làm mượt: 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ằng
requestAnimationFramecho cảm giác mượt như bơ, có quán tính. - Cả một lưới: làm lưới 3×3 card nghiêng. (Bài học: cho mỗi cái một container
perspectiveriêng, hoặc chung một cái cho khung cảnh thống nhất — thử cả hai.)
Phần tiếp theo
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. Đó là kỹ thuật đứng sau vô số UI “cao cấp”.
Ở 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.