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.

5 MIN READ

Ngồi xuống đi, đồ đệ. 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.

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. Trong CSS, cái máy ảnh đó là một property: perspective.

Nghịch demo trước — kéo slider, cảm nhận chiều sâu, rồi mới đọc lý do.

Mở demo đầy đủ:

Ba trục — chỉ vào mặt con

Trước khi code, khắc bức tranh này vào đầu. Một trang web có ba trục:

        Y (up, negative)


        └──────── X (right, positive)


    Z (out of the screen — toward your face)
  • X chạy trái↔phải (con đã dùng với translateX).
  • Y chạy lên↔xuống.
  • 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.

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). Đó là chiều sâu. Đó là toàn bộ cuộc chơi.

Vì sao lần thử đầu trông vẫn phẳng

Đệ tử nào cũng làm thế này rồi hoảng:

.box {
  transform: translateZ(100px); /* ...nothing happens? */
}

Cái box chẳng nhúc nhích. Con không làm sai gì cả — chỉ là con quên cái máy ảnh. 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.

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: con số bị hiểu lầm nhiều nhất trong CSS

perspective là khoảng cách từ mắt người xem tới màn hình. Và đây là phần phản trực giác mà sư phụ phải nhắc lại:

perspective nhỏ = 3D mạnh, kịch tính hơn. Lớn = phẳng hơn.

Hãy nghĩ tới ống kính máy ảnh. 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. Lùi ra cuối phòng (perspective lớn, ~1600px) thì nó gần như phẳng.

Với đa số UI, 800px–1200px cho cảm giác tự nhiên. Chỉ xuống dưới ~400px khi con muốn sự kịch tính.

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); }

Khác biệt rất quan trọng: 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: máy ảnh đứng ở đâu

Mặc định máy ảnh nhìn chính giữa. 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 */
}

Kéo perspective-origin X/Y trong demo và xem panel lộ ra mặt khác.

Bộ ba xoay

Khi đã có máy ảnh, các phép xoay trở thành 3D. 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 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ũ). rotateXrotateY mới là nơi chiều sâu cư ngụ.

Ghép lại: flip card

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. 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 */

Đọc lại bốn dòng comment đó — đó là toàn bộ mô hình tư duy. 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.

Một nhúm JS để bật/tắt class:

card.addEventListener('click', () => card.classList.toggle('is-flipped'));

Lời cảnh báo của sư phụ

  • 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 đặ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 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ạ.

Luyện tập, không thì coi như chưa học

Đọc không phải là học. Làm mấy bài này trước Phần 2:

  1. Nhấc khi hover: cho wrapper của button một perspectivetransform: translateZ(20px) khi hover. Làm nó nhô lên về phía con trỏ.
  2. Thí nghiệm ống kính: 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 khi hover: dựng lại flip card để nó flip khi :hover (không JS).
  4. Tìm lỗi: 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ó.

Phần tiếp theo

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. Đó đã là nhiều 3D hơn cả phần lớn dev đi làm từng học.

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”.