jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

CSS 3D from Scratch · Part 5 — Lighting, Performance & Accessibility

The production finish for CSS 3D: fake directional lighting so objects look solid, keep animations smooth on the GPU compositor, fix z-fighting, honor prefers-reduced-motion, add fallbacks, and debug 3D when it breaks. With a live demo.

9 MIN READ

The final lesson, padawan. {Bài học cuối, đồ đệ.} You can already build cubes, tilt cards, and carousels. {Con đã dựng được cube, card nghiêng, và carousel.} But there’s a wide canyon between “works in my demo” and “ships to real users on real devices.” {Nhưng có một hẻm núi rộng giữa “chạy trong demo của tôi” và “ship cho người dùng thật trên thiết bị thật.”} Today the master teaches you to cross it: light, speed, and care. {Hôm nay sư phụ dạy con băng qua nó: ánh sáng, tốc độ, và sự tử tế.}

This is what separates a flashy CodePen from a 3D effect you can put in production without an apology. {Đây là thứ phân biệt một CodePen lòe loẹt với một hiệu ứng 3D con có thể đưa vào production mà không phải xin lỗi.}

Toggle shading, will-change, z-fighting, and reduced-motion in the demo as you read. {Bật/tắt shading, will-change, z-fighting, và reduced-motion trong demo khi con đọc.}

Open the full demo {Mở demo đầy đủ}: /tools/css-3d-showcase-demo/.

1. Lighting — because CSS has none {Ánh sáng — vì CSS không có}

Here’s a humbling truth: CSS 3D has no lights. {Một sự thật khiêm tốn: CSS 3D không có đèn.} A spinning cube where every face is the same flat color looks like a confusing blob — your eye can’t tell the faces apart. {Một cube xoay mà mọi mặt cùng một màu phẳng trông như cục bùng nhùng — mắt con không phân biệt được các mặt.} Toggle shading: off in the demo and watch it dissolve into nonsense. {Bật shading: off trong demo và xem nó tan thành vô nghĩa.}

The fix is to fake a light. {Cách sửa là giả một nguồn sáng.} Pick an imaginary light direction (say, top-front), then darken each face in proportion to how much it faces away from that light. {Chọn một hướng sáng tưởng tượng (ví dụ top-front), rồi làm tối mỗi mặt tỉ lệ với mức nó quay lưng lại nguồn sáng đó.} The simplest technique: a semi-transparent black overlay per face. {Kỹ thuật đơn giản nhất: một lớp phủ đen bán trong suốt cho mỗi mặt.}

.face::after {
  content: "";
  position: absolute; inset: 0;
  background: #000;          /* shadow overlay */
}
/* light from top-front: top brightest, bottom & back darkest */
.face.top::after    { opacity: 0;    }
.face.front::after  { opacity: 0.05; }
.face.right::after  { opacity: 0.30; }
.face.left::after   { opacity: 0.42; }
.face.back::after   { opacity: 0.55; }
.face.bottom::after { opacity: 0.62; }

That gradient of brightness across faces is the entire illusion of solidity. {Cái dải độ sáng khác nhau giữa các mặt đó chính là toàn bộ ảo giác về độ đặc.} For curved or premium looks, layer in: {Cho vẻ cong hoặc cao cấp, thêm:}

  • a linear-gradient on each face instead of flat color (gives a sheen), {một linear-gradient trên mỗi mặt thay cho màu phẳng (cho vẻ bóng),}
  • the brightness() / contrast() filters to tune faces, {các filter brightness() / contrast() để chỉnh mặt,}
  • a soft box-shadow or a blurred radial “contact shadow” under the object so it sits on a surface instead of floating. {một box-shadow mềm hoặc “bóng tiếp xúc” radial mờ dưới vật thể để nó đặt trên bề mặt thay vì lơ lửng.}

2. Performance — stay on the compositor {Hiệu năng — ở lại trên compositor}

The single most important performance fact in all of CSS animation: {Sự thật hiệu năng quan trọng nhất trong toàn bộ CSS animation:}

transform and opacity are animated by the GPU compositor — no layout, no repaint. Almost everything else (top, left, width, margin…) forces the browser to recompute the page every frame. {transformopacity được GPU compositor animate — không layout, không repaint. Gần như mọi thứ khác (top, left, width, margin…) buộc trình duyệt tính lại trang mỗi khung hình.}

Lucky you: all of CSS 3D is transform. {May cho con: toàn bộ CSS 3D chính là transform.} So 3D is naturally fast — if you don’t sabotage it. {Nên 3D vốn nhanh — nếu con không tự phá.} Rules: {Quy tắc:}

  • Animate only transform/opacity. Never animate left/top/width to move 3D things. {Chỉ animate transform/opacity. Đừng bao giờ animate left/top/width để dời vật 3D.}
  • will-change: transform promotes an element to its own GPU layer before it moves, killing the first-frame hitch: {will-change: transform nâng phần tử lên lớp GPU riêng trước khi nó di chuyển, diệt cú giật khung hình đầu:}
.spinning-thing { will-change: transform; }

But — and the master is stern here — will-change is not a magic “go faster” button. {Nhưng — sư phụ nghiêm khắc chỗ này — will-change không phải nút “nhanh lên” thần kỳ.} Each promoted layer eats GPU memory. {Mỗi lớp được nâng ngốn bộ nhớ GPU.} Put it on 200 elements and you’ll crash performance, not boost it. {Đặt nó lên 200 phần tử và con sẽ phá sập hiệu năng, không phải tăng.} Use it on the few things actively animating, and remove it when they stop. {Chỉ dùng trên vài thứ đang animate, và bỏ khi chúng dừng.}

Open DevTools › Layers (or Rendering › “Layer borders”) to see which elements got their own layer. {Mở DevTools › Layers để thấy phần tử nào có lớp riêng.}

3. Z-fighting — when surfaces argue {Z-fighting — khi các mặt cãi nhau}

Toggle the z-fighting panel in the demo: two flat panels at the exact same depth flicker and tear as the view shifts. {Bật panel z-fighting trong demo: hai panel phẳng ở đúng cùng độ sâu nhấp nháy và rách khi góc nhìn đổi.} The GPU genuinely can’t decide which is on top, so it picks differently pixel-by-pixel, frame-by-frame. {GPU thật sự không quyết được cái nào ở trên, nên chọn khác nhau từng pixel, từng khung hình.}

The fix is trivial once you know it — separate them by a hair: {Cách sửa rất đơn giản khi đã biết — tách chúng ra một sợi tóc:}

.panel-on-top { transform: translateZ(1px); } /* 1px is enough to settle the fight */

You’ll hit this whenever you stack a label on a face, or overlap two coplanar surfaces. {Con sẽ gặp nó mỗi khi xếp một nhãn lên một mặt, hoặc chồng hai mặt đồng phẳng.} Remember the symptom (shimmering/tearing) and the cure (translateZ(1px)). {Nhớ triệu chứng (lung linh/rách) và thuốc chữa (translateZ(1px)).}

4. Accessibility — motion can hurt {Khả năng tiếp cận — chuyển động có thể gây hại}

This is not optional, padawan. {Cái này không tùy chọn, đồ đệ.} Big 3D motion — spins, parallax, zooms — can trigger real nausea, dizziness, and migraines for people with vestibular disorders. {Chuyển động 3D lớn — xoay, parallax, zoom — có thể gây buồn nôn, chóng mặt, đau nửa đầu thật cho người có rối loạn tiền đình.} The platform gives you a way to respect them: {Nền tảng cho con cách tôn trọng họ:}

@media (prefers-reduced-motion: reduce) {
  .spinning-thing { animation: none; }
  .tilt-card { transition: none; }
}

Don’t just delete the motion and leave a dead element — when sensible, replace it with a calm alternative (a static state, or a quick cross-fade instead of a flip). {Đừng chỉ xóa chuyển động rồi để lại phần tử chết — khi hợp lý, thay nó bằng một lựa chọn nhẹ nhàng (trạng thái tĩnh, hoặc cross-fade nhanh thay cho flip).} And for JS-driven effects (the tilt in Part 3), check the same setting before attaching handlers: {Và với hiệu ứng do JS (cái nghiêng ở Phần 3), kiểm tra cùng thiết lập đó trước khi gắn handler:}

const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) stage.addEventListener('pointermove', tilt);

Also: 3D is decoration. Keep the real content in normal DOM order, label interactive 3D with aria-*, and make sure keyboard users can operate carousels (Part 4). {Ngoài ra: 3D là trang trí. Giữ nội dung thật theo thứ tự DOM bình thường, gắn nhãn aria-* cho 3D tương tác, và đảm bảo người dùng bàn phím điều khiển được carousel (Phần 4).}

5. Fallbacks & debugging {Fallback & gỡ lỗi}

CSS 3D support is excellent today, but be a professional: {Hỗ trợ CSS 3D hôm nay rất tốt, nhưng hãy chuyên nghiệp:}

  • Feature-query the fancy bits if you rely on something newer, and ship a flat version otherwise: {Dùng feature query cho phần xịn nếu con phụ thuộc cái mới, và ship bản phẳng nếu không:}
@supports (transform-style: preserve-3d) {
  .card { transform-style: preserve-3d; }
}
  • Degrade gracefully: the flat card should still be readable and usable without the 3D — 3D is the enhancement, not the content. {Suy giảm duyên dáng: card phẳng vẫn phải đọc và dùng được khi không có 3D — 3D là phần nâng cao, không phải nội dung.}

When 3D breaks (and it will), run the master’s checklist: {Khi 3D hỏng (và nó sẽ hỏng), chạy checklist của sư phụ:}

  1. Is perspective on the parent? No camera = flat. (Part 1) {perspective ở cha chưa? Không máy ảnh = phẳng. (Phần 1)}
  2. Is transform-style: preserve-3d set on every 3D container? It’s not inherited. (Part 2) {transform-style: preserve-3d đặt ở mọi container 3D chưa? Không di truyền. (Phần 2)}
  3. Did something flatten it? overflow, filter, opacity < 1, clip-path, or mask on a 3D parent forces flat. {Có gì làm dẹp phẳng nó? overflow, filter, opacity < 1, clip-path, hoặc mask trên cha 3D ép về flat.}
  4. Backface showing through? Add backface-visibility: hidden. (Part 1–2) {Mặt sau lộ ra? Thêm backface-visibility: hidden. (Phần 1–2)}
  5. Flickering? Z-fighting — translateZ(1px). {Nhấp nháy? Z-fighting — translateZ(1px).}

Memorize that list. It solves ~95% of “my CSS 3D is broken” moments. {Thuộc danh sách đó. Nó giải quyết ~95% khoảnh khắc “CSS 3D của tôi hỏng rồi”.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. Light your cube {Chiếu sáng cube}: take your Part 2 cube and add per-face shading overlays for a top-front light. Then move the light to bottom-left by swapping the opacities. {lấy cube Phần 2 và thêm lớp phủ shading mỗi mặt cho nguồn sáng top-front. Rồi dời sáng xuống bottom-left bằng cách hoán đổi opacity.}
  2. Add a contact shadow {Thêm bóng tiếp xúc}: place a blurred dark ellipse beneath a 3D object so it looks grounded. {đặt một elip tối mờ dưới vật thể 3D để nó trông có điểm tựa.}
  3. Make everything reduced-motion safe {Làm mọi thứ an toàn reduced-motion}: revisit your tilt card and carousel; add the media query and the JS guard so they calm down when the OS asks. {xem lại card nghiêng và carousel; thêm media query và guard JS để chúng dịu lại khi OS yêu cầu.}
  4. Hunt a flattening bug {Săn lỗi dẹp phẳng}: deliberately add overflow: hidden to a working 3D scene, confirm it flattens, then fix it. Internalize the checklist. {cố tình thêm overflow: hidden vào một cảnh 3D đang chạy, xác nhận nó dẹp, rồi sửa. Khắc cốt checklist.}

The master bows {Sư phụ cúi chào}

Five parts ago you didn’t know the Z-axis existed. {Năm phần trước con còn không biết trục Z tồn tại.} Now you can place a perspective camera, build solid objects with preserve-3d, make them tilt toward a cursor with layered parallax, spin them into carousels and coverflow, and finish them with lighting, performance, accessibility, and graceful fallbacks. {Giờ con đặt được máy ảnh perspective, dựng vật thể đặc với preserve-3d, làm chúng nghiêng theo con trỏ với parallax phân lớp, xoay thành carousel và coverflow, và hoàn thiện bằng ánh sáng, hiệu năng, khả năng tiếp cận, và fallback duyên dáng.}

That is the entire craft of CSS 3D — no WebGL, no libraries, just perspective, transform-style, translateZ, and the rotation trio, used with taste. {Đó là toàn bộ nghề CSS 3D — không WebGL, không thư viện, chỉ perspective, transform-style, translateZ, và bộ ba xoay, dùng có gu.}

Now go build something the web hasn’t seen yet. {Giờ hãy đi dựng thứ mà web chưa từng thấy.} The master has taught you all he can; the rest is practice. {Sư phụ đã dạy hết những gì có thể; phần còn lại là luyện tập.} 🥋