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.

Why this reference exists {Vì sao có bài tham khảo này}

Most developers learn @keyframes by copy-pasting a spinner {Phần lớn dev học @keyframes bằng cách copy một cái spinner}, then never revisit the model {rồi không bao giờ xem lại mô hình}. That works until you need an element to stay at its final frame {Cách đó ổn cho tới khi bạn cần một phần tử đứng yên ở frame cuối}, run two animations at once {chạy hai animation cùng lúc}, or pause motion on hover {hoặc tạm dừng chuyển động khi hover}. Then the gaps show {Lúc đó lỗ hổng lộ ra}.

This post is a complete, senior-level map of CSS keyframe animations {Bài này là bản đồ hoàn chỉnh, cấp senior về CSS keyframe animation}: how to define keyframes {cách định nghĩa keyframes}, every longhand property {mọi property longhand}, the animation shorthand and its ordering traps {shorthand animation và các bẫy thứ tự}, animation-fill-mode explained concretely {animation-fill-mode giải thích cụ thể}, multiple animations on one element {nhiều animation trên một phần tử}, animation events {sự kiện animation}, and the decision between animation and transition {và lựa chọn giữa animationtransition}.

Tune every longhand property and watch the generated animation: shorthand update live {Chỉnh mọi property longhand và xem shorthand animation: sinh ra cập nhật trực tiếp}.

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


The mental model {Mô hình tư duy}

A CSS animation has two halves {Một CSS animation gồm hai nửa}: a @keyframes rule that describes what changes over a normalized 0%–100% timeline {một rule @keyframes mô tả cái gì thay đổi theo timeline chuẩn hóa 0%–100%}, and a set of animation-* properties applied to an element that describe how that timeline plays {và một tập property animation-* gắn lên phần tử mô tả cách timeline đó chạy}.

The keyframes are reusable {Keyframes tái sử dụng được}. The same @keyframes fade can be played by ten elements with ten different durations {Cùng một @keyframes fade có thể được mười phần tử chạy với mười duration khác nhau}. The keyframes carry no timing — they are pure geometry over a 0→1 progress value {Keyframes không mang timing — chúng là hình học thuần trên giá trị tiến trình 0→1}.


Defining @keyframes {Định nghĩa @keyframes}

The simplest form uses from and to, which are aliases for 0% and 100% {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;
  }
}

For anything beyond a two-endpoint motion, use percentage steps {Với chuyển động vượt quá hai điểm đầu cuối, dùng các mốc phần trăm}. Percentages give you control points along the timeline {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);
  }
}

You can group identical frames in one selector list {Bạn có thể gộp các frame giống nhau vào một danh sách selector}, which keeps repetitive motion compact {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);
  }
}

Rules that matter {Những quy tắc đáng nhớ}

  • If 0% or 100% is omitted, the browser synthesizes it from the element’s computed value for that property {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 đó}. This is subtle and a common source of “why did it jump?” bugs {Điều này tinh tế và là nguồn gốc của bug “sao nó nhảy vậy?”}.
  • Properties not listed in a given keyframe are simply not animated at that point {Property không có trong một keyframe sẽ không được animate tại điểm đó}.
  • A property that cannot be interpolated (e.g. display) historically jumps at the keyframe boundary rather than tweening {Property không nội suy được (vd display) trước đây nhảy tại biên keyframe thay vì tween}.
  • You can set a per-keyframe animation-timing-function to change easing between frames {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%);
  }
}

The longhand properties {Các property longhand}

There are eight longhand properties {Có tám property longhand}. Knowing each one in isolation makes the shorthand trivial {Nắm từng cái riêng lẻ khiến shorthand trở nên đơn giản}.

animation-name {tên animation}

References one or more @keyframes rules by name, or none to disable {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;
}

animation-duration {thời lượng}

How long one cycle takes {Một chu kỳ kéo dài bao lâu}. Required for the animation to be visible — the default 0s means it completes instantly {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;
}

animation-timing-function {hàm timing}

The easing curve applied across the cycle {Đường cong easing áp dụng xuyên suốt chu kỳ}: ease (default), linear, ease-in, ease-out, ease-in-out, cubic-bezier(...), or steps(...) {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);
}

animation-delay {độ trễ}

How long to wait before the first cycle starts {Đợi bao lâu trước khi chu kỳ đầu bắt đầu}. Negative values are powerful: they start the animation already in progress {Giá trị âm rất mạnh: nó khởi động animation đã chạy được một đoạn}, which is the classic trick for staggering many copies of the same loop {đâ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;
}

animation-iteration-count {số lần lặp}

A number, or infinite {Một con số, hoặc infinite}. Fractional counts stop partway through a cycle {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;
}

animation-direction {hướng}

Controls whether each cycle plays forward, backward, or alternates {Đ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... */
}

animation-fill-mode {chế độ fill}

What styles apply before the first frame and after the last {Style nào áp dụng trước frame đầu và sau frame cuối}. Covered in depth below {Phân tích sâu bên dưới}.

animation-play-state {trạng thái chạy}

running or paused {running hoặc paused}. Toggling this is the cheapest way to pause/resume without JavaScript timers {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;
}

The animation shorthand {Shorthand animation}

The shorthand packs all eight longhands into one declaration {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;
}

The full order, per spec {Thứ tự đầy đủ theo spec}, is: name duration timing-function delay iteration-count direction fill-mode play-state {là: name duration timing-function delay iteration-count direction fill-mode play-state}. Most of these are optional and fall back to their initial value {Phần lớn là tùy chọn và quay về initial value}.

The ordering pitfall you must remember {Bẫy thứ tự bạn phải nhớ}

There are two time values in the shorthand — duration and delay — and they look identical to the parser {Có hai giá trị thời gian trong shorthand — durationdelay — và parser nhìn chúng y hệt}. The rule is unambiguous {Quy tắc rõ ràng}: the first <time> is always duration, the second <time> is always delay {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;
}

A second trap {Bẫy thứ hai}: if your @keyframes name collides with a keyword like ease, infinite, or running {nếu tên @keyframes trùng với keyword như ease, infinite, hoặc running}, the parser may consume it as a value instead of the name {parser có thể nuốt nó như một value thay vì tên}. Never name keyframes after reserved words {Đừ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 */
}

A third trap {Bẫy thứ ba}: the shorthand resets every longhand you omit back to its initial value {shorthand reset mọi longhand bạn bỏ qua về initial value}. If you set animation-fill-mode: forwards on one line, then write a shorthand animation: spin 1s on another, the fill-mode is silently wiped to none {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 in depth {animation-fill-mode chuyên sâu}

This is the single most misunderstood property {Đây là property bị hiểu nhầm nhiều nhất}. By default, an element shows its own styles before the animation starts and after it ends {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 changes which keyframe values “leak” outside the active period {Fill-mode đổi việc giá trị keyframe nào “rò” ra ngoài khoảng đang chạy}.

Consider this animation with a delay {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;
}
  • none (default): during the 1s delay the element uses its normal styles (fully opaque), then snaps to opacity: 0 when the animation starts, animates to 1, then snaps back to its normal styles {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}. Almost never what you want for an entrance {Gần như không bao giờ là thứ bạn muốn cho hiệu ứng vào}.
  • forwards: after the animation ends, the element keeps the final keyframe values {sau khi animation kết thúc, phần tử giữ giá trị keyframe cuối}. Use this so an element stays at its end state {Dùng cái này để phần tử đứng ở trạng thái cuối}.
  • backwards: during the delay, the element pre-applies the first keyframe {trong delay, phần tử áp dụng trước keyframe đầu}. So the card sits at opacity: 0 during the 1s wait instead of flashing visible first {Nên thẻ nằm ở opacity: 0 suốt 1s chờ thay vì nhấp nháy hiện ra trước}.
  • both: applies backwards and forwards — pre-fills the start during delay and holds the end after {áp dụng cả backwards forwards — điền trước phần đầu lúc delay và giữ phần cuối sau}.

For nearly every delayed entrance animation, you want both {Cho gần như mọi animation vào có delay, bạn muốn both}:

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

Why forwards isn’t always honored {Vì sao forwards không phải lúc nào cũng được tôn trọng}

animation-fill-mode: forwards holds the computed keyframe value, but the final visual can still be overridden by a more specific rule, or “lost” if the animation is later removed from the element {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 đó}. If you need a permanent end state that survives class changes, set the real style on the element and use the animation only for the transition into it {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}.


Multiple animations on one element {Nhiều animation trên một phần tử}

Every animation-* property accepts a comma-separated list {Mọi property animation-* nhận một danh sách phân tách bằng dấu phẩy}. Each position lines up across all longhands {Mỗi vị trí khớp nhau xuyên qua tất cả longhand}. This lets one element run independent timelines simultaneously {Đ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;
}

The same with the shorthand — separate each full animation with a comma {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;
}

List-length mismatch {Lệch độ dài danh sách}

If one list is shorter than animation-name, its values cycle to fill the remaining slots {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}. This is convenient but easy to misread {Tiện nhưng dễ đọc nhầm}, so prefer matching lengths explicitly when clarity matters {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;
}

A common real-world combo is separating transform and opacity into two keyframe tracks so they can ease differently {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;
}

Animation events (conceptually) {Sự kiện animation (về mặt khái niệm)}

CSS animations dispatch DOM events you can listen to in JavaScript {CSS animation phát các sự kiện DOM bạn có thể nghe trong JavaScript}. They are the bridge between declarative motion and imperative logic {Chúng là cầu nối giữa motion khai báo và logic mệnh lệnh}.

  • animationstart — fires when the animation begins, after any animation-delay elapses {phát khi animation bắt đầu, sau khi hết animation-delay}.
  • animationend — fires when the animation finishes all iterations {phát khi animation hoàn tất mọi iteration}. The natural place to clean up classes or remove an element after an exit animation {Nơi tự nhiên để dọn class hoặc gỡ phần tử sau animation thoát}.
  • animationiteration — fires at the boundary between iterations, so not on the final one if the count is finite {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}.
  • animationcancel — fires if the animation stops before completing (e.g. the element is hidden or the property is removed) {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();
  }
});

The key detail {Chi tiết then chốt}: event.animationName identifies which keyframes ended {event.animationName cho biết keyframes nào kết thúc}, which is essential when multiple animations share an element {điều thiết yếu khi nhiều animation dùng chung một phần tử}.


animation vs transition {animation và transition}

Both interpolate values over time {Cả hai đều nội suy giá trị theo thời gian}, but they answer different questions {nhưng trả lời các câu hỏi khác nhau}.

Concern {Vấn đề}transitionanimation
Trigger {Kích hoạt}A property value changes (state, class, :hover) {Một property đổi giá trị}Declared on the element; runs on load or when applied {Khai trên phần tử; chạy khi load hoặc khi áp}
Endpoints {Điểm đầu cuối}Exactly two: before → after {Đúng hai: trước → sau}Many keyframes (0%–100%) {Nhiều keyframe}
Looping {Lặp}No native loop {Không loop native}iteration-count: infinite
Multi-step {Nhiều bước}No {Không}Yes {Có}
Runs without state change {Chạy không cần đổi state}No {Không}Yes (e.g. spinners) {Có (vd spinner)}
Reversibility {Khả năng đảo}Automatic when state reverts {Tự động khi state quay lại}Manual via direction / restart {Thủ công qua direction/restart}

Choose transition when {Chọn transition khi}

The motion is a response to a state change with a clear start and end {Chuyển động là phản hồi cho một đổi state có đầu cuối rõ ràng}: hover effects, opening a menu, toggling a class {hiệu ứng hover, mở menu, toggle class}. Transitions interrupt and reverse gracefully {Transition ngắt và đảo chiều mượt mà}, which makes them ideal for interactive, bidirectional UI {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;
}

Choose animation when {Chọn animation khi}

The motion must loop, run on its own without a triggering state change, or pass through intermediate steps {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}: spinners, skeleton shimmers, attention pulses, scripted entrances {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;
}

A simple rule of thumb {Một quy tắc đơn giản}: if the motion has exactly two endpoints and is driven by user interaction, reach for transition {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}. If it loops, has many steps, or must run on its own, reach for animation {Nếu nó loop, có nhiều bước, hoặc phải tự chạy, hãy dùng animation}.


Performance and accessibility notes {Ghi chú hiệu năng và khả năng tiếp cận}

Animate transform and opacity whenever possible {Animate transformopacity bất cứ khi nào có thể}. These can run on the compositor thread and skip layout and paint {Chúng chạy được trên compositor thread và bỏ qua layout và paint}. Animating width, top, or margin forces layout on every frame and stutters {Animate width, top, hoặc margin ép layout mỗi frame và gây giật}.

Always respect users who ask for less motion {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;
  }
}

Summary {Tổng kết}

  • @keyframes describe geometry over a 0%–100% timeline; animation-* properties describe playback {@keyframes mô tả hình học trên timeline 0%–100%; property animation-* mô tả việc phát}.
  • The shorthand’s two <time> values are always read as duration then delay {Hai giá trị <time> trong shorthand luôn đọc là duration rồi delay}.
  • The shorthand resets omitted longhands — beware silent fill-mode wipes {Shorthand reset các longhand bị bỏ qua — coi chừng fill-mode bị xóa âm thầm}.
  • Use fill-mode: both for delayed entrances so they neither flash nor snap back {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}.
  • Comma-separated lists let one element run multiple independent animations {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}.
  • Use event.animationName to tell concurrent animations apart in animationend handlers {Dùng event.animationName để phân biệt các animation đồng thời trong handler animationend}.
  • Transition for two-endpoint, state-driven motion; animation for loops, multi-step, and self-running motion {Transition cho chuyển động hai điểm dựa trên state; animation cho loop, nhiều bước, và tự chạy}.

Further reading {Đọc thêm}