CSS 3D from Scratch · Part 1 — Perspective & the Z-axis
The beginner-friendly start to real CSS 3D: the camera mental model, why nothing looks 3D until you add perspective, moving along the Z-axis with translateZ, the three rotation axes, and building your first flip card. With a live demo.
Sit down, young padawan. {Ngồi xuống đi, đồ đệ.} Today the old master teaches you to bend flat pixels into space — using nothing but CSS, no WebGL, no libraries, no 600 KB of JavaScript. {Hôm nay sư phụ dạy con uốn cong những pixel phẳng vào không gian — chỉ bằng CSS, không WebGL, không thư viện, không 600 KB JavaScript.}
Here’s the first secret most people never learn: the screen is flat, so “3D” is an illusion — and every illusion needs a camera. {Bí mật đầu tiên mà hầu hết không ai dạy: màn hình thì phẳng, nên “3D” là một ảo ảnh — và mọi ảo ảnh đều cần một cái máy ảnh.} In CSS, that camera is one property: perspective. {Trong CSS, cái máy ảnh đó là một property: perspective.}
Play with the demo first — drag the sliders, feel the depth, then read why it works. {Nghịch demo trước — kéo slider, cảm nhận chiều sâu, rồi mới đọc lý do.}
Open the full demo {Mở demo đầy đủ}: /tools/css-3d-perspective-demo/.
The three axes — point at your own face {Ba trục — chỉ vào mặt con}
Before any code, burn this picture into your mind. {Trước khi code, khắc bức tranh này vào đầu.} A web page has three axes: {Một trang web có ba trục:}
Y (up, negative)
│
│
└──────── X (right, positive)
╱
╱
Z (out of the screen — toward your face)
- X runs left↔right (you already use this with
translateX). {X chạy trái↔phải (con đã dùng vớitranslateX).} - Y runs up↔down. {Y chạy lên↔xuống.}
- Z is the new one: it runs out of the screen, straight at your eyeballs. {Z là trục mới: nó chạy ra khỏi màn hình, thẳng vào nhãn cầu của con.}
Positive Z = closer to you (bigger). Negative Z = deeper into the screen (smaller, farther away). {Z dương = gần con hơn (to ra). Z âm = lùi sâu vào màn hình (nhỏ đi, xa hơn).} That’s depth. That’s the whole game. {Đó là chiều sâu. Đó là toàn bộ cuộc chơi.}
Why your first attempt looks flat {Vì sao lần thử đầu trông vẫn phẳng}
Every beginner does this and panics: {Đệ tử nào cũng làm thế này rồi hoảng:}
.box {
transform: translateZ(100px); /* ...nothing happens? */
}
The box doesn’t move. {Cái box chẳng nhúc nhích.} You didn’t do anything wrong — you just forgot the camera. {Con không làm sai gì cả — chỉ là con quên cái máy ảnh.} Without a viewpoint, the browser has no idea how “deep” 100px should look, so it renders the Z movement as nothing. {Không có điểm nhìn, trình duyệt không biết 100px “sâu” thì trông thế nào, nên nó render chuyển động Z thành con số không.}
Add perspective to the parent, and the illusion springs to life: {Thêm perspective vào phần tử cha, ảo ảnh sống dậy ngay:}
.scene {
perspective: 600px; /* the camera: 600px from the screen */
}
.box {
transform: translateZ(100px); /* NOW it leaps toward you */
}
perspective: the most misunderstood number in CSS {perspective: con số bị hiểu lầm nhiều nhất trong CSS}
perspective is the distance from the viewer’s eye to the screen. {perspective là khoảng cách từ mắt người xem tới màn hình.} And here’s the counter-intuitive part the master must repeat: {Và đây là phần phản trực giác mà sư phụ phải nhắc lại:}
Smaller
perspective= stronger, more dramatic 3D. Larger = flatter. {perspectivenhỏ = 3D mạnh, kịch tính hơn. Lớn = phẳng hơn.}
Think of a camera lens. {Hãy nghĩ tới ống kính máy ảnh.} Press your nose against an object (small perspective, ~250px) and the nearest corner balloons while the far edge shrinks — extreme distortion. {Dí mũi vào vật (perspective nhỏ, ~250px) thì góc gần phình to còn cạnh xa co lại — méo cực mạnh.} Step back across the room (large perspective, ~1600px) and it looks almost flat. {Lùi ra cuối phòng (perspective lớn, ~1600px) thì nó gần như phẳng.}
For most UI, 800px–1200px feels natural. {Với đa số UI, 800px–1200px cho cảm giác tự nhiên.} Go below ~400px only when you want the drama. {Chỉ xuống dưới ~400px khi con muốn sự kịch tính.}
There are two ways to set it, and beginners mix them up: {Có hai cách đặt nó, và người mới hay nhầm:}
/* Way 1 — the perspective PROPERTY on the parent.
One shared camera for all children. Use this 95% of the time. */
.scene { perspective: 800px; }
/* Way 2 — the perspective() FUNCTION inside transform on the element itself.
Each element gets its own private camera. Handy for one-off effects. */
.box { transform: perspective(800px) rotateY(40deg); }
The difference matters: {Khác biệt rất quan trọng:} the property gives several children one consistent vanishing point (so a row of cards shares a believable scene); the function gives each element its own, which looks wrong when they sit side by side. {property cho nhiều con chung một điểm tụ (nên một hàng card chia sẻ một khung cảnh đáng tin); function cho mỗi phần tử một điểm tụ riêng, trông sai khi chúng đứng cạnh nhau.}
perspective-origin: where the camera stands {perspective-origin: máy ảnh đứng ở đâu}
By default the camera looks dead-center. {Mặc định máy ảnh nhìn chính giữa.} perspective-origin moves the vanishing point — like walking left so you see the right side of a box. {perspective-origin dịch điểm tụ — như con bước sang trái để thấy mặt phải của cái hộp.}
.scene {
perspective: 800px;
perspective-origin: 25% 30%; /* camera shifts left & up */
}
Slide the perspective-origin X/Y controls in the demo and watch the panel reveal a different side. {Kéo perspective-origin X/Y trong demo và xem panel lộ ra mặt khác.}
The rotation trio {Bộ ba xoay}
Once there’s a camera, rotations become 3D. {Khi đã có máy ảnh, các phép xoay trở thành 3D.} Map them to a human head and you’ll never forget: {Gắn chúng với cái đầu người là con không bao giờ quên:}
.box { transform: rotateX(45deg); } /* nodding "yes" — tips forward/back */
.box { transform: rotateY(45deg); } /* shaking "no" — turns left/right */
.box { transform: rotateZ(45deg); } /* tilting your head — flat clock-spin */
rotateZ is the only one that works without perspective — because it’s just a flat 2D spin in disguise (it’s literally the old rotate()). {rotateZ là cái duy nhất chạy không cần perspective — vì nó chỉ là phép xoay 2D phẳng trá hình (chính là rotate() cũ).} rotateX and rotateY are where the depth lives. {rotateX và rotateY mới là nơi chiều sâu cư ngụ.}
Putting it together: the flip card {Ghép lại: flip card}
Now the master rewards your patience with a real, useful component. {Giờ sư phụ thưởng cho sự kiên nhẫn của con bằng một component thật, hữu dụng.} A flip card is the “hello world” of CSS 3D, and it uses exactly four ideas: {Flip card là “hello world” của CSS 3D, và nó dùng đúng bốn ý tưởng:}
<div class="scene">
<div class="card">
<div class="face front">CLICK ME</div>
<div class="face back">HELLO 3D</div>
</div>
</div>
.scene { perspective: 800px; } /* 1. the camera */
.card {
position: relative;
width: 150px; height: 190px;
transform-style: preserve-3d; /* 2. let children live in 3D space */
transition: transform 0.6s;
}
.card.is-flipped { transform: rotateY(180deg); }
.face {
position: absolute; inset: 0;
backface-visibility: hidden; /* 3. hide a face when it turns away */
}
.face.back { transform: rotateY(180deg); } /* 4. pre-spin the back so it faces out */
Read those four comments again — that’s the entire mental model. {Đọc lại bốn dòng comment đó — đó là toàn bộ mô hình tư duy.} We’ll go deep on transform-style: preserve-3d and backface-visibility in Part 2, because they’re what turn a single card into a real object like a cube. {Ta sẽ đào sâu transform-style: preserve-3d và backface-visibility ở Phần 2, vì chúng biến một tấm card thành một vật thể thật như khối lập phương.}
A pinch of JS to toggle the class: {Một nhúm JS để bật/tắt class:}
card.addEventListener('click', () => card.classList.toggle('is-flipped'));
The master’s warnings {Lời cảnh báo của sư phụ}
- Forgot
perspective? YourtranslateZ/rotateX/rotateYwill look dead. 9 out of 10 “my 3D doesn’t work” bugs are a missing camera on the parent. {Quênperspective?translateZ/rotateX/rotateYsẽ trông như chết. 9/10 lỗi “3D của tôi không chạy” là do thiếu máy ảnh ở phần tử cha.} perspectivegoes on the parent;transformgoes on the child. Don’t put both on the same element unless you’re using theperspective()function on purpose. {perspectiveđặt ở cha;transformđặt ở con. Đừng đặt cả hai trên cùng một phần tử trừ khi con cố tình dùng hàmperspective().}- Z is toward your face. Positive = closer/bigger. Say it out loud until it’s reflex. {Z hướng vào mặt con. Dương = gần hơn/to hơn. Nói to lên cho thành phản xạ.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
Reading is not learning. {Đọc không phải là học.} Do these before Part 2: {Làm mấy bài này trước Phần 2:}
- Hover lift {Nhấc khi hover}: give a button
perspectiveon its wrapper andtransform: translateZ(20px)on hover. Make it rise toward the cursor. {cho wrapper của button mộtperspectivevàtransform: translateZ(20px)khi hover. Làm nó nhô lên về phía con trỏ.} - The lens experiment {Thí nghiệm ống kính}: build one rotated card, then animate its parent’s
perspectivefrom 200px to 1500px. Feel the lens “zoom”. {dựng một card xoay, rồi animateperspectivecủa cha từ 200px đến 1500px. Cảm nhận ống kính “zoom”.} - Flip on hover {Flip khi hover}: rebuild the flip card so it flips on
:hover(no JS) —.card:hover { transform: rotateY(180deg); }. {dựng lại flip card để nó flip khi:hover(không JS).} - Find the bug {Tìm lỗi}: delete
backface-visibility: hiddenfrom the demo’s faces in DevTools and stare at the mess. Now you’ll never forget why it’s there. {xóabackface-visibility: hiddenkhỏi các face trong DevTools và nhìn vào mớ hỗn loạn. Giờ con sẽ không bao giờ quên vì sao cần nó.}
What’s next {Phần tiếp theo}
You can now place a camera, move through depth, rotate on every axis, and flip a card. {Giờ con đã biết đặt máy ảnh, di chuyển trong chiều sâu, xoay trên mọi trục, và lật một tấm card.} That’s more 3D than most working devs ever learn. {Đó đã là nhiều 3D hơn cả phần lớn dev đi làm từng học.}
In Part 2, we graduate from a flat card to a solid object: we’ll master transform-style: preserve-3d and backface-visibility, then assemble six faces into a real, rotatable cube — the moment CSS 3D truly clicks. {Ở Phần 2, ta tốt nghiệp từ tấm card phẳng lên vật thể đặc: làm chủ transform-style: preserve-3d và backface-visibility, rồi ghép sáu mặt thành một khối lập phương xoay được thật sự — khoảnh khắc CSS 3D thực sự “thông”.}