jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

9 MIN READ

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ới translateX).}
  • 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. {perspective nhỏ = 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. {rotateXrotateY 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-3dbackface-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? Your translateZ/rotateX/rotateY will look dead. 9 out of 10 “my 3D doesn’t work” bugs are a missing camera on the parent. {Quên perspective? translateZ/rotateX/rotateY sẽ 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.}
  • perspective goes on the parent; transform goes on the child. Don’t put both on the same element unless you’re using the perspective() 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àm perspective().}
  • 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:}

  1. Hover lift {Nhấc khi hover}: give a button perspective on its wrapper and transform: translateZ(20px) on hover. Make it rise toward the cursor. {cho wrapper của button một perspectivetransform: translateZ(20px) khi hover. Làm nó nhô lên về phía con trỏ.}
  2. The lens experiment {Thí nghiệm ống kính}: build one rotated card, then animate its parent’s perspective from 200px to 1500px. Feel the lens “zoom”. {dựng một card xoay, rồi animate perspective của cha từ 200px đến 1500px. Cảm nhận ống kính “zoom”.}
  3. 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).}
  4. Find the bug {Tìm lỗi}: delete backface-visibility: hidden from the demo’s faces in DevTools and stare at the mess. Now you’ll never forget why it’s there. {xóa backface-visibility: hidden khỏ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-3dbackface-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”.}