jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

CSS @keyframes & the animation Shorthand — A Senior Reference

Master CSS @keyframes and the animation shorthand: longhand properties, fill-mode, direction, iteration, multiple animations, and animation vs transition.

Vì sao có bài tham khảo này

Phần lớn dev học @keyframes bằng cách copy một cái spinner, rồi không bao giờ xem lại mô hình. Cách đó ổn cho tới khi bạn cần một phần tử đứng yên ở frame cuối, chạy hai animation cùng lúc, hoặc tạm dừng chuyển động khi hover. Lúc đó lỗ hổng lộ ra.

Bài này là bản đồ hoàn chỉnh, cấp senior về CSS keyframe animation: cách định nghĩa keyframes, mọi property longhand, shorthand animation và các bẫy thứ tự, animation-fill-mode giải thích cụ thể, nhiều animation trên một phần tử, sự kiện animation, và lựa chọn giữa animationtransition.

Chỉnh mọi property longhand và xem shorthand animation: sinh ra cập nhật trực tiếp.

Mở demo đầy đủ:


Mô hình tư duy

Một CSS animation gồm hai nửa: một rule @keyframes mô tả cái gì thay đổi theo timeline chuẩn hóa 0%–100%, và một tập property animation-* gắn lên phần tử mô tả cách timeline đó chạy.

Keyframes tái sử dụng được. Cùng một @keyframes fade có thể được mười phần tử chạy với mười duration khác nhau. Keyframes không mang timing — chúng là hình học thuần trên giá trị tiến trình 0→1.


Định nghĩa @keyframes

Dạng đơn giản nhất dùng fromto, là bí danh của 0%100%:

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

Với chuyển động vượt quá hai điểm đầu cuối, dùng các mốc phần trăm. Phần trăm cho bạn các điểm điều khiển dọc timeline:

@keyframes bounce {
  0% {
    transform: translateY(0);
  }
  40% {
    transform: translateY(-30px);
  }
  70% {
    transform: translateY(-15px);
  }
  100% {
    transform: translateY(0);
  }
}

Bạn có thể gộp các frame giống nhau vào một danh sách selector, giúp chuyển động lặp lại gọn lại:

@keyframes pulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.08);
  }
}

Những quy tắc đáng nhớ

  • Nếu thiếu 0% hoặc 100%, trình duyệt tự suy ra từ giá trị computed của phần tử cho property đó. Điều này tinh tế và là nguồn gốc của bug “sao nó nhảy vậy?”.
  • Property không có trong một keyframe sẽ không được animate tại điểm đó.
  • Property không nội suy được (vd display) trước đây nhảy tại biên keyframe thay vì tween.
  • Bạn có thể đặt animation-timing-function cho từng keyframe để đổi easing giữa các frame.
@keyframes drop {
  0% {
    transform: translateY(-100%);
    /* easing applied from 0% → 50% */
    animation-timing-function: ease-in;
  }
  50% {
    transform: translateY(0);
    /* easing applied from 50% → 100% */
    animation-timing-function: ease-out;
  }
  100% {
    transform: translateY(-8%);
  }
}

Các property longhand

Có tám property longhand. Nắm từng cái riêng lẻ khiến shorthand trở nên đơn giản.

tên animation

Tham chiếu tới một hoặc nhiều rule @keyframes theo tên, hoặc none để tắt.

.box {
  animation-name: fade-in;
}

thời lượng

Một chu kỳ kéo dài bao lâu. Bắt buộc để animation hiển thị — mặc định 0s nghĩa là hoàn tất tức thì.

.box {
  animation-duration: 600ms;
}

hàm timing

Đường cong easing áp dụng xuyên suốt chu kỳ: ease (mặc định), linear, ease-in, ease-out, ease-in-out, cubic-bezier(...), hoặc steps(...).

.box {
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

độ trễ

Đợi bao lâu trước khi chu kỳ đầu bắt đầu. Giá trị âm rất mạnh: nó khởi động animation đã chạy được một đoạn, đây là mẹo kinh điển để stagger nhiều bản sao của cùng một loop.

.box {
  animation-delay: 200ms;
}
.dot:nth-child(2) {
  /* starts as if 150ms already elapsed */
  animation-delay: -150ms;
}

số lần lặp

Một con số, hoặc infinite. Số lẻ dừng giữa chừng một chu kỳ.

.spinner {
  animation-iteration-count: infinite;
}
.half {
  /* plays one and a half cycles, then stops */
  animation-iteration-count: 1.5;
}

hướng

Điều khiển mỗi chu kỳ chạy xuôi, ngược, hay xen kẽ: normal, reverse, alternate, alternate-reverse normal, reverse, alternate, alternate-reverse.

.breathe {
  animation-direction: alternate; /* 0→100, 100→0, 0→100... */
}

chế độ fill

Style nào áp dụng trước frame đầu và sau frame cuối. Phân tích sâu bên dưới.

trạng thái chạy

running hoặc paused. Bật/tắt cái này là cách rẻ nhất để pause/resume mà không cần timer JavaScript.

.marquee:hover {
  animation-play-state: paused;
}

Shorthand animation

Shorthand gói cả tám longhand vào một khai báo:

.box {
  animation: fade-in 600ms ease-out 200ms 1 normal both running;
}

Thứ tự đầy đủ theo spec, là: name duration timing-function delay iteration-count direction fill-mode play-state. Phần lớn là tùy chọn và quay về initial value.

Bẫy thứ tự bạn phải nhớ

hai giá trị thời gian trong shorthand — durationdelay — và parser nhìn chúng y hệt. Quy tắc rõ ràng: giá trị <time> đầu tiên luôn là duration, cái thứ hai luôn là delay.

/* duration = 200ms, delay = 600ms — probably NOT what you meant */
.box {
  animation: slide 200ms 600ms ease-out;
}

/* duration = 600ms, delay = 200ms — clearer to read */
.box {
  animation: slide 600ms ease-out 200ms;
}

Bẫy thứ hai: nếu tên @keyframes trùng với keyword như ease, infinite, hoặc running, parser có thể nuốt nó như một value thay vì tên. Đừng bao giờ đặt tên keyframes trùng từ khóa.

/* DANGER: "infinite" reads as iteration-count, not the name */
@keyframes infinite {
  /* ... */
}
.box {
  animation: infinite 2s; /* ambiguous / broken */
}

Bẫy thứ ba: shorthand reset mọi longhand bạn bỏ qua về initial value. Nếu bạn đặt animation-fill-mode: forwards ở một dòng, rồi viết shorthand animation: spin 1s ở dòng khác, fill-mode bị xóa âm thầm về none.


animation-fill-mode chuyên sâu

Đây là property bị hiểu nhầm nhiều nhất. Mặc định, phần tử hiển thị style của chính nó trước khi animation bắt đầu và sau khi kết thúc. Fill-mode đổi việc giá trị keyframe nào “rò” ra ngoài khoảng đang chạy.

Xét animation này có delay:

@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: reveal 500ms ease-out 1s;
}
  • trong 1s delay phần tử dùng style bình thường (mờ đặc), rồi nhảy về opacity: 0 khi animation bắt đầu, animate tới 1, rồi nhảy lại style bình thường. Gần như không bao giờ là thứ bạn muốn cho hiệu ứng vào.
  • sau khi animation kết thúc, phần tử giữ giá trị keyframe cuối. Dùng cái này để phần tử đứng ở trạng thái cuối.
  • trong delay, phần tử áp dụng trước keyframe đầu. Nên thẻ nằm ở opacity: 0 suốt 1s chờ thay vì nhấp nháy hiện ra trước.
  • áp dụng cả backwards forwards — điền trước phần đầu lúc delay và giữ phần cuối sau.

Cho gần như mọi animation vào có delay, bạn muốn both:

.card {
  animation: reveal 500ms ease-out 1s both;
}

Vì sao forwards không phải lúc nào cũng được tôn trọng

animation-fill-mode: forwards giữ giá trị keyframe computed, nhưng kết quả vẫn có thể bị một rule cụ thể hơn ghi đè, hoặc “mất” nếu animation bị gỡ khỏi phần tử sau đó. Nếu cần trạng thái cuối vĩnh viễn sống sót qua các lần đổi class, hãy đặt style thật lên phần tử và chỉ dùng animation cho phần chuyển vào.


Nhiều animation trên một phần tử

Mọi property animation-* nhận một danh sách phân tách bằng dấu phẩy. Mỗi vị trí khớp nhau xuyên qua tất cả longhand. Điều này cho một phần tử chạy nhiều timeline độc lập đồng thời.

.badge {
  animation-name: float, glow;
  animation-duration: 3s, 1.5s;
  animation-timing-function: ease-in-out, linear;
  animation-iteration-count: infinite, infinite;
  animation-direction: alternate, normal;
}

Tương tự với shorthand — phân tách mỗi animation đầy đủ bằng dấu phẩy:

.badge {
  animation:
    float 3s ease-in-out infinite alternate,
    glow 1.5s linear infinite;
}

Lệch độ dài danh sách

Nếu một danh sách ngắn hơn animation-name, giá trị của nó lặp vòng để điền các slot còn lại. Tiện nhưng dễ đọc nhầm, nên ưu tiên khớp độ dài tường minh khi cần rõ ràng.

.badge {
  animation-name: float, glow, shake;
  /* 2 durations for 3 names → durations cycle: 3s, 1.5s, 3s */
  animation-duration: 3s, 1.5s;
}

Một combo thực tế phổ biến là tách transformopacity thành hai track keyframe để ease khác nhau:

@keyframes rise {
  from { transform: translateY(16px); }
  to   { transform: translateY(0); }
}
@keyframes appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.item {
  animation:
    rise 400ms cubic-bezier(0.22, 1, 0.36, 1) both,
    appear 250ms ease-out both;
}

Sự kiện animation (về mặt khái niệm)

CSS animation phát các sự kiện DOM bạn có thể nghe trong JavaScript. Chúng là cầu nối giữa motion khai báo và logic mệnh lệnh.

  • phát khi animation bắt đầu, sau khi hết animation-delay.
  • phát khi animation hoàn tất mọi iteration. Nơi tự nhiên để dọn class hoặc gỡ phần tử sau animation thoát.
  • phát tại biên giữa các iteration, nên không phát ở cái cuối nếu count hữu hạn.
  • phát nếu animation dừng trước khi xong (vd phần tử bị ẩn hoặc property bị gỡ).
const card = document.querySelector('.card');

card.addEventListener('animationend', (event) => {
  // event.animationName tells you WHICH animation ended
  // when several run on one element
  if (event.animationName === 'exit') {
    card.remove();
  }
});

Chi tiết then chốt: event.animationName cho biết keyframes nào kết thúc, điều thiết yếu khi nhiều animation dùng chung một phần tử.


animation và transition

Cả hai đều nội suy giá trị theo thời gian, nhưng trả lời các câu hỏi khác nhau.

Vấn đềtransitionanimation
Kích hoạtMột property đổi giá trịKhai trên phần tử; chạy khi load hoặc khi áp
Điểm đầu cuốiĐúng hai: trước → sauNhiều keyframe
LặpKhông loop nativeiteration-count: infinite
Nhiều bướcKhông
Chạy không cần đổi stateKhôngCó (vd spinner)
Khả năng đảoTự động khi state quay lạiThủ công qua direction/restart

Chọn transition khi

Chuyển động là phản hồi cho một đổi state có đầu cuối rõ ràng: hiệu ứng hover, mở menu, toggle class. Transition ngắt và đảo chiều mượt mà, khiến chúng lý tưởng cho UI tương tác, hai chiều.

.toggle {
  opacity: 0.5;
  transition: opacity 200ms ease;
}
.toggle:hover {
  opacity: 1;
}

Chọn animation khi

Chuyển động phải loop, tự chạy không cần đổi state, hoặc đi qua các bước trung gian: spinner, shimmer skeleton, pulse gây chú ý, hiệu ứng vào theo kịch bản.

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
.spinner {
  animation: spin 800ms linear infinite;
}

Một quy tắc đơn giản: nếu chuyển động có đúng hai điểm đầu cuối và do tương tác người dùng dẫn dắt, hãy dùng transition. Nếu nó loop, có nhiều bước, hoặc phải tự chạy, hãy dùng animation.


Ghi chú hiệu năng và khả năng tiếp cận

Animate transformopacity bất cứ khi nào có thể. Chúng chạy được trên compositor thread và bỏ qua layout và paint. Animate width, top, hoặc margin ép layout mỗi frame và gây giật.

Luôn tôn trọng người dùng yêu cầu ít chuyển động:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

Tổng kết

  • @keyframes mô tả hình học trên timeline 0%–100%; property animation-* mô tả việc phát.
  • Hai giá trị <time> trong shorthand luôn đọc là duration rồi delay.
  • Shorthand reset các longhand bị bỏ qua — coi chừng fill-mode bị xóa âm thầm.
  • Dùng fill-mode: both cho hiệu ứng vào có delay để không nhấp nháy lẫn nhảy lại.
  • Danh sách phân tách dấu phẩy cho một phần tử chạy nhiều animation độc lập.
  • Dùng event.animationName để phân biệt các animation đồng thời trong handler animationend.
  • Transition cho chuyển động hai điểm dựa trên state; animation cho loop, nhiều bước, và tự chạy.

Đọc thêm