GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

9 hiệu ứng react css animation không cần library

React css animation là cách thêm chuyển động cho component bằng chính CSS, không cần cài library.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục xếp theo hành trình thật của một component trong React: mới hiện lên màn hình (01), biến mất khi được đóng (02), thành danh sách có phần tử thêm vào bớt đi (03), đổi hình theo state (04), phát lại đúng hiệu ứng ra mắt (05), ngó qua cấu hình build thật nằm phía sau (06), khung màn hình chuyển sang khung khác (07), chờ dữ liệu chưa về (08), cuối cùng là chính trang bị thay (09). Ba mục dạng ý tưởng, tức tổ hợp ít ai dùng trong React, là 01, 02 và 07.

01Fade lúc mount bằng @starting-style

Dùng ở đâu?

Ngay khi component card được mount, @starting-style khai báo giá trị của nó ở thời điểm chưa tồn tại trong DOM, nên opacity tự chuyển mượt từ 0 lên 1 mà không cần cờ "đã mount" nào trong useEffect.

Fade lúc mount bằng @starting-style
Mở trong tab mới

01 · Fade lúc mount bằng @starting-style — Diện tích đo được 19.029% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@starting-style@keyframes mf-mountReact 19

Điểm cần ghi nhớ

Kiểu này hợp với card thông báo render theo điều kiện và phần tử vừa thêm vào danh sách.

@starting-style {
  .mf-card { opacity: 0; transform: translateY(10px) scale(.94); }
}
.mf-card {
  opacity: 1;
  transform: none;
  transition: opacity $duration $easing, transform $duration $easing;
}

02Exit lúc unmount (allow-discrete)

Trên màn hình trông thế nào?

Khi bấm nút đóng, component không unmount ngay: class="is-exiting" gắn vào trước, transition-behavior: allow-discrete làm chậm cú chuyển display, state thật bị xóa trong onTransitionEnd.

Exit lúc unmount (allow-discrete)
Mở trong tab mới

02 · Exit lúc unmount (allow-discrete) — Diện tích đo được 22.772% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

allow-discreteonTransitionEndexit class

Cách tạo nó

Thiếu lớp đệm này thì lúc đóng modal hay toast biến mất, transition opacity và transform không kịp phát đã bị cắt khỏi màn hình.

.ue-dialog {
  display: flex;
  opacity: 1;
  transform: scale(1);
  transition: opacity $duration $easing, transform $duration $easing, display $duration allow-discrete;
}
.ue-dialog.is-exiting {
  display: none;
  opacity: 0;
  transform: scale(.92) translateY(6px);
}

03Animation thêm·xóa trong danh sách

Đây là tình huống gì?

Thêm hay bớt một phần tử trong mảng state thì <li> mới vào theo kiểu mục 01, phần tử bị xóa dùng lại đúng pattern exit class của mục 02 — không animation library nào mà thứ tự vẫn khớp.

Animation thêm/xóa phần tử list
Mở trong tab mới

03 · Animation thêm/xóa phần tử list — Diện tích đo được 14.355% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

key@keyframes la-enter@keyframes la-leave

Cách đọc code

Bạn có thể chuyển thẳng kiểu này sang todo list, dòng giỏ hàng hoặc chồng thông báo.

{items.map((it) => (
  <li
    key={it.id}
    className={`${styles.item} ${it.leaving ? styles.leaving : styles.entering}`}
    onTransitionEnd={() => it.leaving && finishRemove(it.id)}
  >
    {it.label}
    <button className={styles.remove} onClick={() => startRemove(it.id)}>×</button>
  </li>
))}

04Đổi trạng thái bằng toggle class

Chuyện gì xảy ra?

Một state boolean ánh xạ thẳng sang một tên class, còn toàn bộ chuyển động ở giữa do CSS transition đảm nhận.

Chuyển trạng thái bằng toggle class
Mở trong tab mới

04 · Chuyển trạng thái bằng toggle class — Diện tích đo được 1.805% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

useStateclassNametransition

Điểm chính của code

"State quyết định class, CSS quyết định chuyển động" là pattern phổ biến nhất trong React, nên panel mở·đóng hay mục nav đang active dùng được ngay.

.tg-panel:not(.is-open) .tg-panel__chevron { transform: rotate(-90deg); }
.tg-panel:not(.is-open) .tg-panel__body { opacity: 0; transform: scaleY(0); }
.tg-panel__body {
  transform-origin: top;
  transition: opacity $duration $easing, transform $duration $easing;
}

05Chạy lại animation bằng key

Màn hình có gì?

Đổi giá trị key prop là React tháo toàn bộ component đó ra rồi mount lại một instance mới, nhờ vậy hiệu ứng ra mắt vốn chỉ chạy một lần lúc mount có thể phát lại bất cứ lúc nào bạn muốn.

Phát lại animation bằng key prop
Mở trong tab mới

05 · Phát lại animation bằng key prop — Diện tích đo được 4.161% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

key propremount@keyframes

Cơ chế vận hành

Đây là cách react trigger css animation phát lại mà không cần thêm state nào: chạy lại hiệu ứng rung khi trả lời sai, hoặc bung lại hiệu ứng chúc mừng mỗi lần điểm số thay đổi.

function Badge({ value }: { value: number; key?: number }) {
  // Mỗi lần key đổi, React unmount phần tử này rồi remount,
  // nên keyframe pop vốn chỉ chạy một lần lúc mount lại phát lại từ đầu mỗi lần.
  return <span className={styles.badge}>+{value}</span>;
}
// Nơi gọi: <Badge key={score} value={score} />

06Thiết lập CSS Modules + SCSS

Nhìn thấy gì?

Đây là mục lộ ra cấu hình build thật mà chín thư mục react/ của bài cùng dựa vào: file *.module.scss biên dịch thành tên class scoped kiểu styles.foo, style từng component khóa riêng, không xung đột toàn cục.

Cài đặt CSS Modules + SCSS
Mở trong tab mới

06 · Cài đặt CSS Modules + SCSS — Diện tích đo được 1.792% · khung hình chuyển động 9/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

*.module.scssstyles.cardscoped classname

Cách làm

Áp dụng cho mọi dự án React muốn viết SCSS theo phạm vi component.

// styles.card · styles.title không phải chuỗi hardcode,
// mà là tên class trong CssModulesSetup.module.scss được biên dịch thật lúc build.
<div className={styles.term}>
  <div className={styles.accent}>styles.card → "{styles.card}"</div>
  <div className={styles.accent}>styles.title → "{styles.title}"</div>
</div>

07startViewTransition kèm cập nhật state

Hiệu ứng chuyển màn hình React dùng ở đâu?

Bọc một lần cập nhật state của React bằng document.startViewTransition(), trình duyệt tự cross-fade hai ảnh chụp DOM trước và sau, khỏi gắn animation tay cho từng phần tử.

startViewTransition + cập nhật state
Mở trong tab mới

07 · startViewTransition + cập nhật state — Diện tích đo được 30.558% · khung hình chuyển động 6/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

startViewTransitionflushSync@keyframes vt-cross-in

Lý do làm vậy

Nhưng nếu trong callback chỉ gọi setState, auto-batching của React 18 sẽ trì hoãn commit; phải dùng flushSync ép cập nhật chạy đồng bộ thì thời điểm chụp mới khớp.

const toggle = () => {
  const doc = document as DocWithViewTransition;
  const next = mode === "list" ? "grid" : "list";
  if (doc.startViewTransition) {
    doc.startViewTransition(() => flushSync(() => setMode(next)));
  } else {
    setMode(next);
  }
};

08Trao skeleton trong Suspense

Màn hình nào?

Đặt một skeleton shimmer bằng CSS vào chỗ fallback của React.Suspense: khi component lazy sẵn sàng, fallback rút đi và nội dung thật fade vào ngay tại chỗ, màn hình không bị nhảy dựng.

Skeleton trong Suspense
Mở trong tab mới

08 · Skeleton trong Suspense — Diện tích đo được 4.592% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

React.lazySuspense fallbackshimmer

Cốt lõi khi triển khai

Kiểu loading animation css react này hợp với route đã tách code và widget dashboard tải trễ.

.sk-bar {
  background: linear-gradient(90deg, rgba(255,255,255,.1) 25%, rgba(255,255,255,.24) 50%, rgba(255,255,255,.1) 75%);
  background-size: 200% 100%;
  animation: sk-shimmer 1.1s linear infinite;
}
@keyframes sk-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

09Animation chuyển route

Cái gì chuyển động?

Lấy tham số URL làm luôn key của component, hoặc bọc cú đổi route trong startViewTransition: trang cũ và trang mới nối nhau bằng CSS transition thay vì cú cắt tức thời.

Animation chuyển route
Mở trong tab mới

09 · Animation chuyển route — Diện tích đo được 30.983% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

key by routestartViewTransitionexit/enter

Trọng tâm của mục

Điểm mấu chốt: nếu không dùng key, React sẽ tái sử dụng (patch) cùng một component nên chẳng có chu kỳ unmount·mount nào xảy ra.

const navigate = () => {
  const next = (route + 1) % ROUTES.length;
  if (typeof document !== "undefined" && "startViewTransition" in document) {
    (document as any).startViewTransition(() => setRoute(next));
  } else {
    setRoute(next);
  }
};
// <Page key={ROUTES[route]} label={ROUTES[route]} />

Animation trong React dễ hỏng ở đâu — mấy cái bẫy hay gặp

Vấn đề là gì?

Làm thật cả chín mục, lỗi gặp nhiều nhất nằm ở mục 08 (skeleton Suspense). Bản đầu tiên đặt skeleton lẫn nội dung đều position: absolute chồng lên nhau, card bên ngoài không có cao độ riêng, nên card bị bẹp thành một dải 40px — phần tử tuyệt đối không tham gia tính cao độ của cha, cái bẫy quen thuộc với ai từng đụng CSS nhưng bọc trong component React lại khó thấy.

Né nó thế nào?

Một lỗi nữa ở mục 03: thu hẹp còn 320px là chữ đổ xuống hai dòng, cả danh sách vượt cao độ viewport. Trước khi dán mật khẩu dùng để mở chín mục trong file nén là ndwa6jaf theo nguyên văn câu này, hãy ép mỗi mục nằm trên một dòng bằng white-space: nowrap cùng text-overflow: ellipsis. Mục 05 và 09 xoay quanh key cũng có bẫy riêng: đổi nội dung mà không đổi giá trị key thì instance cũ giữ nguyên, animation mount sẽ không bao giờ phát lại.

Tiếp cận — chín demo giữ lại gì dưới reduced-motion

Dưới reduced-motion còn lại gì?

Cả chín demo đều tắt animation lặp vô hạn trong khối prefers-reduced-motion: reduce và nhảy thẳng tới trạng thái cuối: mount thì hiện ra, gấp thì giữ gấp, route thì tới đích. Mục 01 và 02 vốn không phụ thuộc transition để truyền thông tin nên chức năng vẫn nguyên vẹn; riêng mục 05 mang tính "nhắc lại", dưới reduced-motion con số vẫn đổi tức thì nên không mất thông tin.

Còn gì nữa cần để ý?

Cả bản vanilla lẫn bản react trong zip đều giữ nguyên media query đó, không phải sửa gì thêm.

FAQ

Tạo hiệu ứng animation trong React có bắt buộc dùng library không?

Đa số cảnh mount, unmount và đổi trạng thái làm được bằng CSS thuần cùng hook của React như chín cách trong bài, không cần đến một react css animation library nào. Library chỉ chiếm lợi thế khi phải dàn dựng timeline sát sao hay mô phỏng vật lý kiểu kéo thả.

@starting-style chạy trên trình duyệt nào?

Chrome, Edge và bản Opera mới đã hỗ trợ; Safari với Firefox thì tùy phiên bản. Trên trình duyệt chưa hỗ trợ, phần tử hiện ra ngay không kèm transition mà bố cục không vỡ, nên dùng như progressive enhancement là an toàn.

Đổi key prop có làm giảm performance không?

Mỗi lần key đổi là component được tạo lại từ đầu, nên cấp key mới cho cả danh sách đổi liên tục sẽ tốn. Nên chừa cho một phần tử nhỏ cần phát lại như mục 05. Nguyên lý reset state bằng key xem trong tài liệu chính thức của React; các ví dụ đổi trạng thái chỉ bằng một class xem ở 9 kiểu toggle switch và 9 chuyển trạng thái nhỏ như lưu·copy.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.