Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 54 — CSS Animations & Transitions

Web Design Basics Bài 54/60

Mở đầu — vì sao bài này quan trọng

Hãy thử nhớ lại lần gần nhất bạn rê chuột vào một nút bấm trên một website "xịn" — chẳng hạn trang chủ của Tiki hay Shopee. Cái nút không đột ngột đổi màu rồi đứng im. Nó mượt mà chuyển sang màu đậm hơn trong khoảng một phần năm giây, có khi còn nhích lên một chút như đang muốn nói "bấm tôi đi". Cảm giác đó — sự "có hồn" của giao diện — phần lớn đến từ hai công cụ bạn sẽ học hôm nay: transitionanimation trong CSS.

Đây là điều khiến một sản phẩm trông "đắt tiền" hay "rẻ tiền". Một giao diện không có chuyển động nào cảm giác như một tờ giấy chết. Một giao diện chuyển động quá đà, giật cục lại gây khó chịu. Người designer giỏi nằm ở giữa: biết dùng chuyển động đúng lúc, đúng liều, để hướng dẫn ánh mắt người dùng và phản hồi cho mọi hành động của họ.

Trong toàn bộ khóa học, đây là bài "thực thi kỹ thuật" của chủ đề chuyển động. Bài 53 đã cho bạn tư duy về microinteractions — khi nàotại sao nên dùng animation. Bài 54 này tập trung vào làm thế nào: cú pháp CSS cụ thể, các thuộc tính, easing, keyframes, và cách viết sao cho mượt mà mà không làm chậm trang. Học xong bài này, bạn cầm được cây bút để vẽ chuyển động bằng code thật.

Khái niệm cốt lõi

CSS cho bạn hai cơ chế tạo chuyển động. Hiểu rõ sự khác nhau giữa chúng là nền tảng cho mọi thứ phía sau.

Transition — chuyển động khi trạng thái thay đổi

transition là cách bạn nói với trình duyệt: "Khi một thuộc tính CSS thay đổi giá trị, đừng nhảy ngay sang giá trị mới — hãy chuyển dần trong một khoảng thời gian." Transition luôn cần một cú kích hoạt (trigger), thường là :hover, :focus, hoặc khi một class được thêm/xóa bằng JavaScript.

Ví dụ kinh điển từ chính dàn ý của bài:

.button {
  background: blue;
  transition: background 0.2s ease;
}
.button:hover {
  background: darkblue;
}

Ở trạng thái bình thường, nút màu xanh. Khi rê chuột, nó cần thành xanh đậm. Nhờ dòng transition, sự thay đổi đó trải đều trong 0.2 giây thay vì nhảy phắt. Cú pháp đầy đủ của shorthand:

transition: <property> <duration> <timing-function> <delay>;
/ ví dụ /
transition: transform 0.3s ease-in-out 0s;
  • property: thuộc tính nào sẽ được "làm mượt" (ví dụ background, transform, opacity). Dùng all để áp cho mọi thuộc tính, nhưng nên tránh vì kém tối ưu.
  • duration: kéo dài bao lâu, ví dụ 0.2s hoặc 200ms.
  • timing-function: đường cong tốc độ (easing), sẽ nói kỹ bên dưới.
  • delay: chờ bao lâu trước khi bắt đầu.
Bạn có thể khai báo nhiều transition cùng lúc, cách nhau bằng dấu phẩy:

transition: background 0.2s ease, transform 0.3s ease-out;

Animation — chuyển động tự chạy theo kịch bản

transition chỉ đi từ trạng thái A sang trạng thái B. Khi bạn cần một chuyển động nhiều bước, tự chạy (không cần hover), hoặc lặp vô hạn — ví dụ một spinner xoay tròn hoặc một trái tim đập — bạn cần @keyframes và thuộc tính animation.

@keyframes là nơi bạn định nghĩa "kịch bản" — trạng thái tại từng mốc thời gian:

@keyframes pulse {
  0%   { transform: scale(1);    opacity: 1; }
  50%  { transform: scale(1.15); opacity: 0.7; }
  100% { transform: scale(1);    opacity: 1; }
}

.notification-dot { animation: pulse 1.5s ease-in-out infinite; }

Các thuộc tính của animation (shorthand):

animation: <name> <duration> <timing-function> <delay> <iteration-count> <direction> <fill-mode>;
  • iteration-count: số lần lặp, infinite để lặp mãi.
  • direction: normal, reverse, alternate (chạy tới rồi chạy lui).
  • fill-mode: forwards giữ lại trạng thái cuối sau khi chạy xong — rất hay dùng để animation không "nhảy về" trạng thái đầu.

Easing — linh hồn của chuyển động

Đây là phần nhiều bạn mới bỏ qua, nhưng nó quyết định chuyển động cảm giác tự nhiên hay máy móc. Easing là đường cong mô tả tốc độ thay đổi theo thời gian.

  • linear: tốc độ đều tăm tắp. Cảm giác cứng, robot. Chỉ hợp với chuyển động xoay liên tục (spinner).
  • ease: mặc định — bắt đầu chậm, nhanh dần, chậm lại cuối. Khá tự nhiên.
  • ease-out: bắt đầu nhanh, chậm dần. Tốt cho phần tử xuất hiện vào màn hình — cảm giác "đáp xuống" nhẹ nhàng.
  • ease-in: bắt đầu chậm, nhanh dần. Tốt cho phần tử biến mất.
  • cubic-bezier(...): tự định nghĩa đường cong. Ví dụ cubic-bezier(0.34, 1.56, 0.64, 1) tạo hiệu ứng nảy nhẹ (overshoot) rất "đã" cho các nút.
Nguyên tắc của dân chuyên: vật trong thế giới thực không bao giờ chuyển động linear. Xe phải tăng tốc và phanh. Vì vậy giao diện dùng easing đúng sẽ luôn cảm giác "thật" hơn.

Hiệu năng — chỉ animate hai thứ rẻ

Đây là kiến thức tách bạn khỏi người mới. Trình duyệt vẽ trang qua nhiều giai đoạn: layout (tính kích thước/vị trí) → paint (tô màu) → composite (ghép lớp lên màn hình). Animate width, height, top, left, margin buộc trình duyệt tính lại layout mỗi khung hình — rất tốn, gây giật (janky).

Ngược lại, hai thuộc tính transform (di chuyển, xoay, phóng to) và opacity chỉ chạy ở giai đoạn composite, được GPU xử lý, mượt như bơ ngay cả trên điện thoại yếu. Quy tắc vàng: ưu tiên animate transformopacity. Muốn dịch chuyển một phần tử, dùng transform: translateX(20px) thay vì left: 20px.

Tình huống thực tế

Ví dụ 1 — Nút "Thêm vào giỏ" của một sàn TMĐT Việt Nam

Một bạn junior designer làm cho startup TMĐT giả định "ChợViet" được giao làm nút "Thêm vào giỏ". Phiên bản đầu, nút đổi từ xám sang cam ngay khi hover — không transition. QA phản hồi: "Cảm giác bị khựng, như trang bị lag." Bạn ấy thêm vào:

.add-to-cart {
  background: #e0e0e0;
  transform: translateY(0);
  transition: background 0.2s ease, transform 0.15s ease-out, box-shadow 0.2s ease;
}
.add-to-cart:hover {
  background: #f5512c;
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(245, 81, 44, 0.35);
}
.add-to-cart:active {
  transform: translateY(0);
}

Kết quả: nút giờ "nhô lên" nhẹ khi hover, đổ bóng cam mềm, và "nhấn xuống" khi click — đúng phản hồi vật lý người dùng mong đợi. Trong một A/B test giả định, biến thể có chuyển động này tăng tỷ lệ click nút thêm giỏ khoảng 4–6% so với nút "chết". Bài học: chuyển động không chỉ làm đẹp, nó tạo affordance — gợi ý cho người dùng rằng phần tử này có thể tương tác.

Ví dụ 2 — Loading spinner cho app giao đồ ăn

Đội frontend của một app giao đồ ăn (kiểu GrabFood/ShopeeFood) cần spinner trong lúc chờ tải danh sách quán. Một bạn dùng GIF nặng 80KB. Bạn senior thay bằng pure CSS:

@keyframes spin {
  to { transform: rotate(360deg); }
}
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #eee;
  border-top-color: #00b14f;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

Chỉ vài dòng, không tải file, không tốn băng thông, mượt trên cả máy yếu. Lưu ý ở đây linearđúng — vòng xoay đều mới tự nhiên, dùng ease sẽ thấy nó giật từng nhịp. Bài học: đúng công cụ cho đúng việc. Spinner xoay là một trong số rất ít trường hợp linear thắng ease. Và CSS animation thường thay thế được file ảnh động, vừa nhẹ vừa sắc nét trên màn hình retina.

Ví dụ 3 — Modal trượt lên trên mobile và bài học prefers-reduced-motion

Một team làm app fintech tại TP.HCM thiết kế modal xác nhận chuyển tiền trượt từ dưới lên (bottom sheet). Họ viết:

@keyframes slideUp {
  from { transform: translateY(100%); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.sheet {
  animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

Mọi thứ ổn cho đến khi một người dùng lớn tuổi phản hồi rằng các hiệu ứng trượt khiến họ chóng mặt. Đây là vấn đề accessibility thật. Họ bổ sung:

@media (prefers-reduced-motion: reduce) {
  .sheet {
    animation: none;
  }
}

prefers-reduced-motion là tùy chọn hệ điều hành (iOS/Android/Windows) cho người nhạy cảm với chuyển động. Tôn trọng nó là chuẩn mực chuyên nghiệp. Bài học: chuyển động đẹp phải đi kèm trách nhiệm — luôn có "đường thoát" cho người dùng không chịu được animation.

Hướng dẫn từng bước

Giả sử bạn muốn làm một card sản phẩm "nâng lên" khi hover. Làm theo trình tự sau, đây cũng là quy trình tư duy bạn nên áp dụng cho mọi animation:

  • Xác định trạng thái đầu và cuối. Trạng thái nghỉ: card phẳng, bóng nhẹ. Trạng thái hover: card nhô lên 4px, bóng đậm hơn. Viết CSS cho cả hai trạng thái trước, chưa cần transition.
  • Quyết định animate thuộc tính nào. Nâng lên → dùng transform: translateY(-4px) (không dùng margin-top). Bóng → box-shadow. Tránh đụng tới width/height/top.
  • Thêm transition vào trạng thái nghỉ (không phải vào :hover):
.product-card {
  transform: translateY(0);
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  transition: transform 0.25s ease-out, box-shadow 0.25s ease-out;
}
.product-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0,0,0,0.15);
}

Đặt transition ở trạng thái nghỉ để cả lúc "vào" hover và lúc "ra" khỏi hover đều mượt.

  • Chọn duration hợp lý. Microinteraction giao diện thường nằm trong khoảng 150–300ms. Dưới 100ms gần như không cảm nhận được; trên 500ms bắt đầu cảm giác chậm chạp.
  • Chọn easing. Phần tử xuất hiện/nâng lên → ease-out. Cần nảy nhẹ → cubic-bezier.
  • Kiểm tra hiệu năng và accessibility. Mở DevTools (tab Performance hoặc Rendering → bật "Paint flashing") xem có bị repaint thừa không. Thêm khối @media (prefers-reduced-motion: reduce) nếu animation đáng kể.
Với animation lặp (@keyframes), quy trình tương tự nhưng bước 1 đổi thành "vẽ kịch bản từng mốc 0%/50%/100%".

Lỗi thường gặp & mẹo

Lỗi: đặt transition vào trạng thái :hover thay vì trạng thái gốc. Khi đó lúc rê chuột vào thì mượt, nhưng lúc rê ra thì nhảy phắt. Luôn đặt transition ở selector gốc.

Lỗi: dùng transition: all. Nó áp transition lên mọi thuộc tính thay đổi, kể cả những thứ bạn không ngờ tới, dễ gây giật và khó debug. Hãy liệt kê rõ từng thuộc tính.

Lỗi: animate width, height, top, left. Gây giật trên mobile. Hầu hết trường hợp thay được bằng transform: scale() hoặc translate().

Lỗi: quên animation-fill-mode: forwards. Animation chạy xong "nhảy" về trạng thái đầu, gây chớp giật. Thêm forwards để giữ trạng thái cuối.

Lỗi: lạm dụng chuyển động. Mọi thứ đều bay, đều nảy → giao diện rối và rẻ tiền. Chuyển động nên kín đáo; người dùng cảm nhận được "mượt" mà không nói được chính xác vì sao.

Mẹo: dùng will-change: transform cho phần tử sắp animate phức tạp để báo trình duyệt chuẩn bị tối ưu — nhưng dùng tiết kiệm, lạm dụng sẽ tốn bộ nhớ.

Mẹo: thời lượng nên tỉ lệ với quãng đường. Phần tử di chuyển xa (modal toàn màn hình) cần lâu hơn (300–400ms); phần tử nhỏ (nút) nên nhanh (150–200ms).

Mẹo: trang web Easings.net và công cụ cubic-bezier.com giúp bạn "nghịch" đường cong easing trực quan trước khi dán vào code.

Bài tập thực hành

  • Nút có hồn: Tạo một nút với background xanh, khi hover đổi sang xanh đậm, nâng lên 2px và có bóng. Khi :active thì nhấn xuống. Dùng transition cho cả ba thuộc tính, duration 200ms, easing ease-out.
  • Skeleton shimmer: Viết @keyframes tạo hiệu ứng "lấp lánh" chạy ngang (gradient di chuyển) cho một ô skeleton loading, lặp vô hạn, duration 1.5s. Gợi ý: animate background-position.
  • Card nâng lên: Áp dụng đúng 6 bước trong phần Hướng dẫn để làm một product card hover. Mở DevTools bật "Paint flashing", xác nhận chỉ có composite, không có layout/paint thừa.
  • Tôn trọng người dùng: Lấy bất kỳ animation nào ở trên, bọc lại bằng @media (prefers-reduced-motion: reduce) để tắt hoặc giảm chuyển động. Bật tùy chọn "Reduce Motion" trong cài đặt máy để kiểm tra thật.
  • Thử thách easing: Tạo một icon trái tim "đập" (scale 1 → 1.3 → 1) bằng @keyframes, thử lần lượt linear, ease-in-out, và một cubic-bezier có overshoot. Tự cảm nhận sự khác biệt và viết một câu mô tả cái nào hợp nhất.

Tóm tắt

  • Transition làm mượt sự thay đổi giữa hai trạng thái và cần một trigger (hover, focus, đổi class). Animation + @keyframes tạo chuyển động nhiều bước, tự chạy, có thể lặp.
  • Cú pháp transition: transition: property duration timing-function delay. Đặt nó ở trạng thái gốc, không phải ở :hover.
  • Easing là linh hồn: ease-out cho phần tử xuất hiện, ease-in cho biến mất, linear chỉ cho spinner, cubic-bezier để tùy biến và tạo nảy.
  • Hiệu năng: ưu tiên animate transformopacity — chúng chạy trên GPU, mượt mà. Tránh width/height/top/left.
  • Thời lượng microinteraction lý tưởng: 150–300ms.
  • Luôn tôn trọng prefers-reduced-motion — chuyển động đẹp phải đi cùng accessibility.
  • Chuyển động giỏi là chuyển động kín đáo: người dùng cảm thấy "mượt" mà không nhận ra vì sao. Đó chính là dấu ấn của một designer chuyên nghiệp.