9 CSS Keyframe Animations — dán 1 dòng là chạy
CSS keyframe animation là chuyển động lặp được định nghĩa bằng @keyframes rồi gộp vào một class duy nhất, dán vào phần tử nào cũng chạy ngay.
Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip
- 01 Bồng bềnh lên xuống
- 02 Bật nảy
- 03 Xoay tròn
- 04 Nhấp nháy nhẹ
- 05 Rung lắc
- 06 Lắc lư
- 07 Nhấp nháy
- 08 Nhịp tim
- 09 Dẻo như thạch
Chín class không xếp theo bảng chữ cái mà theo vai trò mà một phần tử đảm nhận trên màn hình: báo hiệu đang tồn tại (01, 02), báo hiệu đang chờ (03, 04), thu hút sự chú ý (05, 06), rồi báo hiệu trạng thái vừa đổi (07 đến 09). Ở khung lưới phía trên, cả chín class tự lặp lại vì iframe cố định không nhận được sự kiện con trỏ thật — riêng 05 và 09 còn có thêm một keyframes lặp -loop riêng gắn theo class .is-demo để giả lập một cú bấm mỗi 2 giây, còn bản vanilla trong zip chỉ chạy đúng lúc bạn tự bấm.
01Bồng bềnh lên xuống
Chỉ cần gắn .a-float, một vòng keyframes di chuyển translateY lên xuống chậm rãi chạy ngay, khiến phần tử nào cũng trông như đang lơ lửng. Hợp cho huy hiệu minh họa hay ảnh mockup ở phần hero, nơi cần liên tục nhắc người xem rằng phần tử ấy vẫn đang hiện diện.
$duration: 3s;
$easing: ease-in-out;
.a-float { animation: float $duration $easing infinite; }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
02Bật nảy
.a-bounce lặp lại nhịp chạm đất rồi bật cao bằng translateY như một quả bóng nảy, dùng chuyển động để ra hiệu rằng vị trí đó có thể bấm được. Dùng cho mũi tên gợi ý cuộn trang hoặc icon "xem thêm", chỗ cần một cử chỉ mời gọi chứ không phải chữ.
$duration: 1s;
$easing: cubic-bezier(.28, .84, .42, 1);
.a-bounce { animation: bounce $duration $easing infinite; }
@keyframes bounce {
0%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
55% { transform: translateY(10px); }
75% { transform: translateY(-2px); }
90% { transform: translateY(4px); }
}
03Xoay tròn
.a-spin không chỉ dành riêng cho spinner tải. Nó xoay rotate từ 0 đến 360 độ bằng linear không ngắt quãng, nên gắn thẳng vào icon làm mới hay icon đồng bộ cũng hợp lý không kém.
$duration: 1s;
$easing: linear; // không gia giảm tốc độ thì vòng xoay mới không giật
.a-spin { animation: spin $duration $easing infinite; }
@keyframes spin {
to { transform: rotate(360deg); }
}
04Nhấp nháy nhẹ
.a-pulse đưa scale qua lại giữa 1 và 1.06 đồng thời hạ rồi nâng opacity, tạo điểm nhấn kín đáo mà không buộc trình duyệt vẽ lại layout. Hợp cho chấm trạng thái đang live, badge tính năng mới, hoặc một nút CTA cần thêm chút chú ý.
$duration: 2s;
$easing: ease-out;
.a-pulse { animation: pulse $duration $easing infinite; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.06); opacity: .92; }
}
05Rung lắc
.a-shake không phải riêng cho lỗi form. Nó đẩy translateX qua lại sáu lần thật ngắn rồi tự dừng trong 0.5 giây, dùng chung cho bất kỳ lời nhắc "nhìn vào đây" nào — rung một badge tính năng mới hay đánh dấu một bước trong hướng dẫn sử dụng.
$duration: 500ms;
$easing: cubic-bezier(.36, .07, .19, .97);
$amount: 8px;
.a-shake.is-shaking { animation: shake $duration $easing 1; }
@keyframes shake {
10%, 90% { transform: translateX($amount * -.5); }
20%, 80% { transform: translateX($amount); }
30%, 70% { transform: translateX($amount * -1); }
40%, 60% { transform: translateX($amount * .7); }
50% { transform: translateX($amount * -.7); }
}
06Lắc lư
.a-wobble chồng translateX và rotate lệch thời điểm nhau trong cùng một khối keyframes, tạo chuyển động bất định và sống động hơn hẳn rung lắc đơn thuần ở mục 05. Hợp cho gợi ý một thẻ có thể kéo hoặc nhấn mạnh một mục danh sách vừa được sắp xếp lại.
$duration: 1s;
$easing: ease-in-out;
.a-wobble { animation: wobble $duration $easing infinite; }
@keyframes wobble {
0%, 100% { transform: translateX(0) rotate(0deg); }
15% { transform: translateX(-6px) rotate(-4deg); }
30% { transform: translateX(5px) rotate(3deg); }
45% { transform: translateX(-4px) rotate(-2deg); }
60% { transform: translateX(3px) rotate(2deg); }
75% { transform: translateX(-2px) rotate(-1deg); }
}
07Nhấp nháy
.a-flash đưa opacity qua 1→0→1 hai lần liên tiếp, nhấp nháy rõ ràng mà chưa bao giờ khiến phần tử biến mất hẳn. Dùng cho icon thông báo khẩn, chấm báo đang phát trực tiếp, hoặc một badge cần lóe sáng một lần.
$duration: 1s;
$easing: ease-in-out;
.a-flash { animation: flash $duration $easing infinite; }
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: .15; }
}
08Nhịp tim
.a-heartbeat chia keyframes thành nhịp "đập, đập, nghỉ" với hai lần bật scale ngắn rồi một quãng nghỉ, mô phỏng đúng nhịp không đều của một nhịp tim thật thay vì một chu kỳ đều tăm tắp. Hợp cho trạng thái chờ của icon thích hoặc một chấm trạng thái trên dashboard giám sát thời gian thực.
$duration: 1.2s;
$easing: ease-in-out;
.a-heartbeat { animation: heartbeat $duration $easing infinite; }
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.24); }
28% { transform: scale(1); }
42% { transform: scale(1.18); }
70% { transform: scale(1); }
}
09Dẻo như thạch
.a-jelly di chuyển scaleX và scaleY lệch pha ngược nhau, khiến phần tử móp lại như thạch ngay khi vừa nhấn rồi rung nhẹ trở về hình dạng cũ. Hợp cho icon xác nhận đã thêm vào giỏ hàng hoặc phản hồi nảy nhẹ trên một nút sau khi click.
$duration: 600ms;
$easing: cubic-bezier(.68, -0.55, .27, 1.55);
.a-jelly.is-jellying { animation: jelly $duration $easing 1; }
@keyframes jelly {
0% { transform: scale(1, 1); }
20% { transform: scale(1.25, .75); }
40% { transform: scale(.75, 1.25); }
60% { transform: scale(1.15, .85); }
80% { transform: scale(.95, 1.05); }
100% { transform: scale(1, 1); }
}
Vì sao thứ tự animation rút gọn lại như vậy?
Viết dồn cả tám giá trị vào một dòng animation: rất dễ nhầm duration với delay nếu không nhớ đúng thứ tự. Trang animation rút gọn của MDN xếp thứ tự như bảng dưới.
| Vị trí | Thuộc tính con | Ví dụ ở bài này | Giá trị mặc định |
|---|---|---|---|
| 1 | animation-name |
float |
none |
| 2 | animation-duration |
3s |
0s |
| 3 | animation-timing-function |
ease-in-out |
ease |
| 4 | animation-delay |
(không khai thì 0) | 0s |
| 5 | animation-iteration-count |
infinite |
1 |
| 6 | animation-direction |
(không khai thì normal) | normal |
| 7 | animation-fill-mode |
(không khai thì none) | none |
| 8 | animation-play-state |
(không khai thì running) | running |
animation-fill-mode quyết định giá trị của keyframes có giữ lại trước và sau khi animation chạy hay không. 05 rung lắc và 09 dẻo như thạch chỉ phát một lần (1) nhưng khung 0% và 100% đều là hình dạng gốc, nên không cần fill-mode, phần tử vẫn tự quay về đúng vị trí ban đầu. Nếu khung cuối của keyframes khác hình dạng gốc, phải thêm forwards thì hình dạng đó mới được giữ lại sau khi animation kết thúc.
Nó dễ hỏng ở đâu — cái bẫy
Gắn hai class tiện ích như .a-float và .a-spin lên cùng một phần tử tưởng chừng cộng dồn hai chuyển động, nhưng cả hai đều khai báo trọn vẹn thuộc tính rút gọn animation, nên theo quy tắc cascade của CSS, class đứng sau trong stylesheet đè hoàn toàn lên class đứng trước và chỉ một chuyển động còn sống. Muốn chồng hai chuyển động lên cùng một phần tử phải liệt kê tên trong animation-name: float, spin; rồi liệt kê animation-duration và animation-timing-function với đúng số lượng giá trị tương ứng. Nếu hai animation cùng tác động lên một thuộc tính như transform, chế độ mặc định replace vẫn khiến cái sau ghi đè cái trước; animation-composition: add cộng dồn hai giá trị transform lại thay vì đè, và thuộc tính này đã có mặt rộng rãi trên các trình duyệt lớn từ tháng 7 năm 2023.
Lúc mới dựng mục 04 nhấp nháy nhẹ, hiệu ứng chỉ gắn trên một chấm nhỏ nên diện tích pixel đổi màu quá ít, không vượt ngưỡng cường độ mà công cụ đo dùng để xác nhận có chuyển động thật. Đổi sang gắn .a-pulse cho cả nút thay vì riêng chấm nhỏ mới lên đủ ngưỡng và trông rõ là một điểm nhấn. Chín file đã đóng gói sẵn giá trị đúng, mở bằng mật khẩu nft4q6u2 gõ đúng như nó hiện trên dòng này, không thêm khoảng trắng nào.
Khả năng tiếp cận
Khi hệ thống bật chế độ giảm chuyển động, cả chín class đều tắt bằng @media (prefers-reduced-motion: reduce), đưa animation về none. Riêng 05 rung lắc không chỉ tắt animation mà còn đổi nền sang màu báo lỗi để thông tin "cần chú ý" không mất đi cùng chuyển động, còn tám class kia dừng lại ở đúng trạng thái tĩnh của chúng. Mức hỗ trợ trình duyệt cho media query này nằm trong bảng tương thích trên trang prefers-reduced-motion của MDN.
@media (prefers-reduced-motion: reduce) {
.a-float, .a-bounce, .a-spin, .a-pulse,
.a-wobble, .a-flash, .a-heartbeat { animation: none; }
.a-shake.is-shaking, .a-jelly.is-jellying { animation: none; }
}
Các bài CSS animation khác nằm trong chuyên mục CSS, còn trang giới thiệu nói rõ site này đưa ra thứ gì.
FAQ
Chỉ cần một trong chín class này thôi có được không?
Được. Mỗi class đóng gói gọn trong một khối @keyframes riêng và không chia sẻ biến hay trạng thái với mục khác, nên copy đúng phần SCSS của một mục cùng đoạn HTML đi kèm là chạy độc lập ngay. Mục 01 và 03 là hai bản đơn giản nhất nếu trang chỉ cần một chuyển động.
Kết hợp hai animation trên cùng một phần tử thì làm sao?
Gắn hai class cùng lúc sẽ khiến class đứng sau đè hết class đứng trước vì cả hai khai animation trọn vẹn. Liệt kê nhiều tên trong animation-name cách nhau bằng dấu phẩy, và nếu hai animation cùng tác động lên transform thì thêm animation-composition: add để cộng dồn thay vì ghi đè — ví dụ cụ thể nằm ở mục bẫy phía trên.
React thay việc toggle class như thế nào?
Các component trong thư mục react/ của file zip nhận duration, easing, color qua props rồi đổ vào biến CSS. Với 05 rung lắc và 09 dẻo như thạch — hai class chỉ phát một lần — component dùng useState để bật class trạng thái ngay khi bấm rồi tắt lại ở sự kiện onAnimationEnd, để lần bấm sau lại phát từ đầu.