CSS Transforms for Animation — 2D, 3D, and the Compositor Win
Senior guide to animating CSS transforms: translate/scale/rotate, transform-origin, individual transform properties, 3D and perspective, and why transforms are cheap.
Transform là công cụ chủ lực của animation web hiệu năng cao. Nếu chỉ học sâu một primitive animation CSS, hãy chọn cái này.
Bài này đi từ 2D cơ bản đến card flip 3D thật, và kết bằng lý do transform luôn thắng top/left/width.
Kéo slider để dựng chuỗi transform trực tiếp, so sánh thứ tự hàm, và xem motion trên compositor vs top/left.
Mở demo đầy đủ:
What transform actually does
Property transform áp dụng phép biến đổi hình học sau khi layout đã tính xong. Element giữ nguyên box gốc trong document flow — các phần tử xung quanh không bị xê dịch.
Chính điều đó làm transform rẻ: nó không bao giờ kích hoạt layout (reflow) của trang.
.box {
/* Visually shifts the box without affecting siblings' positions. */
transform: translateX(40px);
}
2D transforms: the four primitives
Có bốn hàm transform 2D cốt lõi. Mỗi hàm nhận một hoặc hai tham số và có thể kết hợp trong cùng một giá trị transform.
.translate {
/* Move along X / Y. Percentages are relative to the element's own size. */
transform: translate(20px, -10px);
}
.scale {
/* Grow/shrink. 1 = original, 0.5 = half, 2 = double. */
transform: scale(1.2); /* uniform */
transform: scale(1.2, 0.8); /* x, y separately */
}
.rotate {
/* Positive = clockwise. Units: deg, rad, turn. */
transform: rotate(15deg);
}
.skew {
/* Slants the element. Rarely animated, great for stylistic shears. */
transform: skew(10deg, 0deg);
}
Một chi tiết quan trọng về translate với phần trăm: translateX(100%) dịch element theo chiều rộng của chính nó, không phải của cha. Điều này làm cho hiệu ứng trượt vào từ ngoài màn hình trở nên đơn giản.
.drawer {
/* Park the drawer fully off-screen to the left. */
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.drawer.open {
transform: translateX(0);
}
transform-origin: the pivot point
Mọi transform diễn ra quanh một điểm gốc. Mặc định đó là tâm element (50% 50%). Đổi nó để xoay hoặc scale quanh một góc hay cạnh.
.spinner {
/* Rotate around the top-left corner instead of the center. */
transform-origin: top left; /* keywords */
transform-origin: 0 0; /* equivalent length form */
transform: rotate(45deg);
}
.fan-card {
/* Cards fanning out from a bottom-center pivot, like a hand of cards. */
transform-origin: bottom center;
transform: rotate(-8deg);
}
transform-origin nhận giá trị thứ ba cho trục Z, điều này quan trọng khi bạn bước vào 3D.
Order matters: transforms are not commutative
Các hàm transform áp dụng theo thứ tự phải sang trái về mặt khái niệm, nhưng quy tắc thực tế là: thứ tự liệt kê làm đổi kết quả. Translate rồi rotate khác với rotate rồi translate.
/* A: move right 100px, THEN rotate around the new position's origin. */
.a {
transform: translateX(100px) rotate(45deg);
}
/* B: rotate first (axes now tilted), THEN translate along the tilted X. */
.b {
transform: rotate(45deg) translateX(100px);
}
Ở trường hợp A element trượt 100px sang phải, rồi xoay tại chỗ. Ở trường hợp B element xoay trước, nên translateX sau đó dịch nó theo đường chéo theo trục đã xoay.
Khi trộn chúng, hãy đọc chuỗi như một pipeline hệ tọa độ: mỗi hàm biến đổi không gian tọa độ mà hàm kế tiếp thao tác lên.
Individual transform properties (2022+)
CSS hiện đại expose translate, rotate, và scale thành các property độc lập. Chúng đã là baseline trên mọi trình duyệt hiện đại từ 2022–2023.
.card {
translate: 20px 0; /* like translate(20px, 0) */
rotate: 15deg; /* like rotate(15deg) */
scale: 1.1; /* like scale(1.1) */
}
How they compose with transform
Trình duyệt ghép chúng theo thứ tự cố định: translate, rồi rotate, rồi scale, và cuối cùng là nội dung trong property transform.
.composed {
translate: 50px 0;
rotate: 30deg;
scale: 1.2;
/* Applied last, on top of the three above. */
transform: skewX(8deg);
}
Lợi ích thực tế lớn: bạn có thể animate từng trục độc lập mà không ghi đè các trục khác.
/* With the legacy single `transform`, hover would WIPE the rotate. */
.legacy:hover {
transform: scale(1.1); /* ❌ loses any rotate set elsewhere */
}
/* Individual properties keep concerns separate. */
.modern {
rotate: 5deg; /* set once, stays put */
transition: scale 0.2s ease;
}
.modern:hover {
scale: 1.1; /* ✅ rotate is untouched */
}
Điều này cũng mở khóa transition và keyframes riêng cho từng property — ví dụ duration khác nhau cho scale và rotate.
Stepping into 3D
Transform 3D thêm trục Z hướng ra khỏi màn hình về phía người xem. Để thấy chiều sâu, bạn cần perspective — thiếu nó, các phép xoay 3D trông phẳng lì.
perspective and perspective-origin
perspective đặt khoảng cách từ người xem đến mặt phẳng Z=0. Giá trị nhỏ = méo mạnh, kịch tính hơn.
/* Option 1: perspective on the PARENT applies to all 3D children. */
.scene {
perspective: 800px;
perspective-origin: 50% 50%; /* vanishing point */
}
/* Option 2: the perspective() function inside a single element's transform. */
.lonely-card {
transform: perspective(800px) rotateY(45deg);
}
Dùng perspective ở cha khi nhiều con cần chung một điểm tụ nhất quán. Dùng hàm perspective() cho một element riêng lẻ độc lập.
The 3D rotation and translation functions
.r {
rotateX: 45deg; /* tilt forward/back, around the horizontal axis */
/* shorthand forms inside transform: */
transform: rotateX(45deg); /* pitch */
transform: rotateY(45deg); /* yaw — the classic flip */
transform: rotateZ(45deg); /* roll — same as 2D rotate() */
transform: translateZ(50px); /* push toward / away from viewer */
}
translateZ chỉ tạo hiệu ứng thấy được khi perspective đang bật — nếu không, dịch theo Z chẳng thay đổi gì trên phép chiếu phẳng.
transform-style: preserve-3d
Mặc định các con bị làm phẳng vào mặt phẳng của cha. transform-style: preserve-3d bảo trình duyệt giữ các con ở đúng không gian 3D. Đây là bắt buộc cho scene 3D lồng nhau và card flip.
.cube {
transform-style: preserve-3d; /* children live in 3D, not flattened */
}
backface-visibility
Khi một mặt xoay quá 90°, bạn nhìn thấy mặt sau của nó. backface-visibility: hidden ẩn mặt sau bị lật gương đó — thiết yếu để mặt sau của thẻ không lòi qua.
.face {
backface-visibility: hidden; /* hide the mirrored back of each face */
}
Building a 3D card flip
Giờ ghép tất cả lại: một scene có perspective, một container bên trong xoay với preserve-3d, và hai mặt với backface-visibility: hidden.
<div class="card-scene">
<div class="card">
<div class="card-face card-front">Front</div>
<div class="card-face card-back">Back</div>
</div>
</div>
.card-scene {
/* The viewport into 3D space; perspective lives on the parent. */
perspective: 1000px;
width: 240px;
height: 320px;
}
.card {
position: relative;
width: 100%;
height: 100%;
/* Keep both faces in real 3D so the back can sit behind the front. */
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Flip on hover (or toggle a class via JS for click/tap). */
.card-scene:hover .card {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 12px;
/* Hide the mirrored reverse of each face. */
backface-visibility: hidden;
}
.card-front {
background: #1a1a1a;
}
.card-back {
background: #c8ff00;
color: #000;
/* Pre-rotate the back so it faces away until the flip completes. */
transform: rotateY(180deg);
}
Mẹo: mặt sau được xoay sẵn 180°, nên ban đầu nó quay đi. Khi .card xoay 180°, mặt sau quay về phía bạn còn mặt trước quay đi. backface-visibility: hidden đảm bảo chỉ mặt đang hướng về bạn được vẽ.
Để flip bằng click thay vì hover, toggle một class với vài dòng JS.
const card = document.querySelector('.card');
card.addEventListener('click', () => {
card.classList.toggle('is-flipped');
});
/* Then drive the flip from the class instead of :hover. */
.card.is-flipped {
transform: rotateY(180deg);
}
Why transform + opacity is the compositor win
Đây là phần kỹ sư senior quan tâm nhất. Trình duyệt render theo các giai đoạn, thường gọi là pixel pipeline:
- Layout — tính hình học box (vị trí, kích thước).
- Paint — tô pixel (màu, chữ, shadow).
- Composite — ghép các layer đã paint lên màn hình, có thể trên GPU.
Animate top, left, width, hay height làm đổi hình học, nên trình duyệt phải chạy lại layout → paint → composite mỗi frame. Ở 60fps đó là khối lượng lớn trong ngân sách 16ms, và thường rớt frame.
/* ❌ Janky: every frame triggers layout + paint for the whole subtree. */
.bad {
position: relative;
left: 0;
transition: left 0.3s ease;
}
.bad:hover {
left: 100px;
}
transform và opacity đặc biệt: chúng có thể xử lý hoàn toàn ở giai đoạn composite, trên GPU, bỏ qua layout và paint. Element đã được paint thành một layer; compositor chỉ việc dịch, scale, hoặc làm mờ texture đó.
/* ✅ Smooth: only the composite stage runs each frame. */
.good {
transform: translateX(0);
transition: transform 0.3s ease;
}
.good:hover {
transform: translateX(100px);
}
will-change — promote a layer, sparingly
will-change gợi ý trình duyệt thăng element lên layer compositor riêng từ trước. Dùng nó ngay trước animation, không phải toàn cục.
.menu {
/* Tell the browser a transform animation is coming. */
will-change: transform;
}
Lạm dụng will-change tạo quá nhiều layer và ngốn bộ nhớ, có thể làm hại hiệu năng thay vì giúp. Chỉ áp dụng cho vài element thực sự animate, và cân nhắc gỡ bỏ sau khi animation kết thúc.
Mental model for choosing properties
Một heuristic đơn giản: nếu một property thay đổi mà không làm dịch element nào khác, nó nhiều khả năng thân thiện với compositor. transform và opacity đạt điều đó; các property hình học thì không.
- Cần di chuyển? Dùng
translate, khôngtop/left. - Cần đổi kích thước? Dùng
scale, khôngwidth/height. - Cần làm mờ? Dùng
opacity.
Một lưu ý: scale kéo giãn nội dung đã paint (kể cả chữ), nên có thể hơi mờ giữa animation; thường đây là đánh đổi chấp nhận được để mượt.
Wrap-up
Transform rẻ vì nó né layout và paint, để GPU compositor làm việc. Nắm vững các primitive 2D, tôn trọng việc thứ tự quan trọng, dùng các property translate/rotate/scale độc lập để ghép sạch sẽ, và dùng 3D với perspective + preserve-3d + backface-visibility khi cần chiều sâu.
Trên hết: animate transform và opacity, giao diện của bạn sẽ mượt như bơ ở 60fps.