CSS 3D from Scratch · Part 2 — preserve-3d & Building Real Objects
Turn flat cards into solid objects: master transform-style: preserve-3d and backface-visibility, understand the local coordinate system, then assemble six faces into a real, rotatable CSS cube — step by step. With a live cube builder demo.
Welcome back, padawan. {Mừng con trở lại, đồ đệ.} In Part 1 you learned to place a camera and flip a single card. {Ở Phần 1 con đã học đặt máy ảnh và lật một tấm card.} A card is flat, though — today we build a thing with volume, a real object you can spin and look around: the cube. {Nhưng card thì phẳng — hôm nay ta dựng một thứ có thể tích, một vật thể thật xoay được và ngắm quanh được: khối lập phương.}
This is the lesson where CSS 3D stops being a party trick and clicks. {Đây là bài học khiến CSS 3D thôi là trò mua vui và thực sự “thông”.} And it all hinges on one property most tutorials hand-wave: transform-style: preserve-3d. {Và tất cả xoay quanh một property mà phần lớn tutorial cho qua loa: transform-style: preserve-3d.}
Rotate the cube, slide “explode” to pull the faces apart, and toggle preserve-3d off to watch it collapse. {Xoay cube, kéo “explode” để tách các mặt ra, và tắt preserve-3d để xem nó sụp xuống.}
Open the full demo {Mở demo đầy đủ}: /tools/css-3d-cube-demo/.
The property that makes objects possible {Property làm cho vật thể trở nên khả thi}
Here’s the trap. {Đây là cái bẫy.} By default, when you apply 3D transforms to children, the browser quietly flattens them back onto their parent’s plane after rendering. {Mặc định, khi con áp transform 3D lên các phần tử con, trình duyệt lặng lẽ dẹp phẳng chúng về mặt phẳng của cha sau khi render.} The default value is transform-style: flat. {Giá trị mặc định là transform-style: flat.}
That means a child pushed “behind” the parent and a child pulled “in front” all get squashed into the same flat layer — like pressing a pop-up book shut. {Nghĩa là con bị đẩy “ra sau” và con bị kéo “ra trước” đều bị ép vào cùng một lớp phẳng — như gập cuốn sách pop-up lại.}
.cube {
transform-style: preserve-3d; /* "kids, you may live in 3D space" */
}
transform-style: preserve-3dtells an element to keep its children in the same 3D space, instead of flattening them. {transform-style: preserve-3dbảo một phần tử giữ các con trong cùng không gian 3D, thay vì dẹp phẳng chúng.}
In the demo, hit preserve-3d: off. {Trong demo, bấm preserve-3d: off.} The cube instantly dies and becomes a flat stack of squares. {Cube chết ngay và biến thành một chồng ô vuông phẳng.} Turn it back on — volume returns. {Bật lại — thể tích trở về.} That’s the whole reason the property exists. {Đó là toàn bộ lý do property này tồn tại.}
The rule of thumb the master lives by: {Quy tắc sư phụ luôn nhớ:}
Every element that has 3D-positioned children needs
preserve-3d. It is NOT inherited — each nesting level that needs depth must declare it. {Mọi phần tử có con đặt trong 3D đều cầnpreserve-3d. Nó KHÔNG di truyền — mỗi cấp lồng nhau cần chiều sâu phải tự khai báo.}
How six squares become a cube {Sáu ô vuông thành cube như thế nào}
This is the part that looks like wizardry but is pure logic. {Đây là phần trông như phép thuật nhưng thuần logic.} A cube has six faces. {Cube có sáu mặt.} Each face is the same square, just aimed in a different direction and then pushed outward by half the cube’s size. {Mỗi mặt là cùng một ô vuông, chỉ là hướng về một phía khác rồi đẩy ra ngoài bằng nửa kích thước cube.}
For a 110px cube, “half” is 55px. {Với cube 110px, “nửa” là 55px.} Watch the pattern — rotate to aim, then translateZ to push out: {Để ý quy luật — xoay để nhắm, rồi translateZ để đẩy ra:}
.scene { perspective: 700px; } /* camera (Part 1) */
.cube {
position: relative;
width: 110px; height: 110px;
transform-style: preserve-3d; /* the magic word */
}
.face {
position: absolute;
inset: 0; /* stack all six in the same spot */
width: 110px; height: 110px;
}
/* Aim each face, then push it out by half the width (55px). */
.front { transform: rotateY(0deg) translateZ(55px); }
.back { transform: rotateY(180deg) translateZ(55px); }
.right { transform: rotateY(90deg) translateZ(55px); }
.left { transform: rotateY(-90deg) translateZ(55px); }
.top { transform: rotateX(90deg) translateZ(55px); }
.bottom { transform: rotateX(-90deg) translateZ(55px); }
Why does this work? Read it like a story, right-to-left (remember from the transforms post: transforms apply right-to-left). {Vì sao nó chạy? Đọc như một câu chuyện, từ phải sang trái (nhớ bài transforms: transform áp dụng từ phải sang trái).} Take .right: first the face is at the center, then translateZ(55px) pushes it 55px toward the camera, then — no wait. {Lấy .right: đầu tiên mặt ở giữa, rồi translateZ(55px) đẩy nó 55px về phía máy ảnh, rồi — khoan.}
Here’s the clean way to think about it. {Cách nghĩ gọn nhất là thế này.} The browser reads rotateY(90deg) translateZ(55px) as: first rotate the face’s whole coordinate system 90° (so its local “out” now points to the right), then translateZ pushes it out along that newly-rotated Z — which is now pointing right. {Trình duyệt đọc rotateY(90deg) translateZ(55px) là: trước xoay cả hệ tọa độ của mặt 90° (để “hướng ra” cục bộ của nó giờ chỉ sang phải), rồi translateZ đẩy nó ra dọc theo Z vừa-xoay-đó — giờ đang chỉ sang phải.}
That’s the key insight of all CSS 3D modeling: {Đó là insight then chốt của mọi việc dựng hình CSS 3D:}
translateZalways moves along the element’s OWN local Z-axis — which rotation has already turned to point wherever you aimed it. {translateZluôn di chuyển dọc theo trục Z CỤC BỘ của phần tử — mà phép xoay đã quay sẵn để chỉ về hướng con nhắm.}
Pull the explode slider in the demo and you’ll see each face slide outward along its own facing direction. That’s translateZ on a rotated axis, made visible. {Kéo slider explode trong demo và con sẽ thấy mỗi mặt trượt ra theo đúng hướng nó đang quay mặt. Đó là translateZ trên trục đã xoay, được hiện hình.}
Rotating the whole cube {Xoay cả khối cube}
Because the cube has preserve-3d, rotating the parent rotates the entire assembled object — all six faces move together as one solid thing: {Vì cube có preserve-3d, xoay phần tử cha sẽ xoay cả vật thể đã ghép — sáu mặt cùng di chuyển như một khối đặc:}
.cube { transform: rotateX(-22deg) rotateY(32deg); }
Drag rotateX/rotateY in the demo. {Kéo rotateX/rotateY trong demo.} You’re not moving six things — you’re moving one object whose insides happen to be six faces. {Con không di chuyển sáu thứ — con di chuyển một vật thể mà bên trong tình cờ là sáu mặt.} That’s the payoff of preserve-3d. {Đó là phần thưởng của preserve-3d.}
backface-visibility — friend of the flip, optional for the cube {backface-visibility — bạn của flip, tùy chọn cho cube}
Every face has a front and a back. {Mỗi mặt có một mặt trước và một mặt sau.} backface-visibility: hidden makes a face disappear the moment it turns away from you. {backface-visibility: hidden làm một mặt biến mất khi nó quay lưng lại với con.}
- Flip card (Part 1): essential — without it, the back face’s text shows through the front, mirrored. {Flip card (Phần 1): thiết yếu — không có nó, chữ của mặt sau hiện xuyên qua mặt trước, bị lật gương.}
- Opaque cube: optional — the faces are solid, so you can’t see through anyway. But
hiddenis a small performance win (the GPU skips drawing hidden faces). {Cube đặc: tùy chọn — các mặt đặc nên đằng nào cũng không nhìn xuyên được. Nhưnghiddenlà một chút lợi hiệu năng (GPU bỏ qua việc vẽ mặt khuất).}
Toggle backface in the demo while the cube auto-spins and watch the inside faces blink in and out. {Bật/tắt backface trong demo khi cube tự xoay và xem các mặt bên trong chớp tắt.}
Spinning it forever {Xoay mãi mãi}
A cube that spins on its own is three lines: {Một cube tự xoay chỉ là ba dòng:}
@keyframes spin {
from { transform: rotateX(-22deg) rotateY(0deg); }
to { transform: rotateX(-22deg) rotateY(360deg); }
}
.cube { animation: spin 12s linear infinite; }
We’ll do far fancier motion in Part 4. {Ta sẽ làm motion xịn hơn nhiều ở Phần 4.} For now, notice it’s one animation on the parent moving the whole solid — not six animations. {Giờ hãy để ý đó là một animation trên cha di chuyển cả khối — không phải sáu animation.}
The master’s warnings {Lời cảnh báo của sư phụ}
preserve-3dis not inherited. If you nest a 3D thing inside a 3D thing, every level that holds depth needs its ownpreserve-3d. {preserve-3dkhông di truyền. Nếu con lồng vật 3D trong vật 3D, mỗi cấp giữ chiều sâu đều cầnpreserve-3driêng.}overflow: hiddensilently killspreserve-3d. Settingoverflow(orclip-path,filter,opacity< 1,mask) on the 3D parent forces flattening. This is the #1 “why did my cube go flat?” bug. {overflow: hiddenâm thầm giếtpreserve-3d. Đặtoverflow(hoặcclip-path,filter,opacity< 1,mask) lên cha 3D sẽ ép dẹp phẳng. Đây là lỗi “sao cube của tôi phẳng?” số 1.}- translateZ = half the size. For an N-pixel cube, push faces out by N/2. Get this wrong and your cube has gaps or overlaps. {translateZ = nửa kích thước. Cube N pixel thì đẩy mặt ra N/2. Sai số này thì cube bị hở hoặc chồng mặt.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Build it from memory {Dựng lại từ trí nhớ}: recreate the six-face cube without looking. The
rotate … translateZ(half)pattern should become muscle memory. {dựng lại cube sáu mặt mà không nhìn. Quy luậtrotate … translateZ(nửa)phải thành phản xạ cơ bắp.} - Make a rectangular box {Làm hộp chữ nhật}: turn the cube into a 120×80×200 box. (Hint: faces are no longer identical squares, and each axis uses a different
translateZ.) {biến cube thành hộp 120×80×200. (Gợi ý: các mặt không còn là ô vuông giống nhau, và mỗi trục dùngtranslateZkhác.)} - A 3D dice {Xúc xắc 3D}: put pip dots (1–6) on the faces and rotate the cube so a chosen number faces front. {đặt chấm (1–6) lên các mặt và xoay cube để một số đã chọn quay ra trước.}
- Break it on purpose {Cố tình làm hỏng}: add
overflow: hiddento the.cubein DevTools and confirm it flattens. Now you’ll recognize this bug in the wild instantly. {thêmoverflow: hiddenvào.cubetrong DevTools và xác nhận nó dẹp phẳng. Giờ con sẽ nhận ra lỗi này ngoài đời ngay lập tức.}
What’s next {Phần tiếp theo}
You can now build solid 3D objects, not just flat cards. {Giờ con dựng được vật thể 3D đặc, không chỉ card phẳng.} You understand the property (preserve-3d), the construction pattern (rotate → translateZ(half)), and the gotchas that flatten a scene. {Con hiểu property (preserve-3d), quy luật dựng hình, và các bẫy làm dẹp phẳng khung cảnh.}
In Part 3, we make 3D respond to the human — a card that tilts toward your mouse in real time, with layered parallax depth that makes it feel like glass floating above the page. This is where 3D becomes delightful UX, not just a demo. {Ở Phần 3, ta làm 3D phản hồi con người — một tấm card nghiêng theo chuột con theo thời gian thực, với chiều sâu parallax phân lớp khiến nó như tấm kính lơ lửng trên trang. Đây là lúc 3D trở thành UX thú vị, không chỉ là demo.}