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

9 CSS Text Animation Effects — dán 1 dòng là chạy

CSS text animation effects là các chuyển động chữ dựng thuần bằng CSS, không cần JavaScript, giúp tiêu đề hay câu chữ xuất hiện, nhấn mạnh hoặc lặp lại.

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

Chín hiệu ứng xếp theo thứ tự chữ trải qua trên màn hình, không phải theo độ nổi tiếng. Năm kiểu đầu (01–05) là lúc chữ xuất hiện lần đầu, rồi đến một trạng thái chờ lặp mãi sau khi đã hiện ra (06), tiếp theo hai kiểu nhấn mạnh khi câu chữ đã đứng yên trên màn hình (07–08), và khép lại bằng một trạng thái trông bất ổn như màn hình vừa lỗi (09). Mỗi mục dưới đây chỉ giữ lại rule cốt lõi, còn file đầy đủ kèm markup nằm trong zip.

01Lộ chữ từ mặt nạ dưới lên

Bạn thấy gì

Tiêu đề nằm trong khung overflow:hidden bị cắt gọn, còn chữ bên trong trượt lên bằng translateY từ ngoài mép dưới về đúng vị trí, lộ ra một lượt như kéo một tấm rèm. Đúng lúc rèm kéo lên, từng chữ còn được gỡ mờ dần qua filter: blur, nên chữ không chỉ trồi lên mà còn như đang lấy nét theo.

Lộ chữ từ mặt nạ dưới lên
Mở trong tab mới

01 · Lộ chữ từ mặt nạ dưới lên — Diện tích đo được 5.417% · khung hình chuyển động 12/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

overflow:hiddentranslateY 마스크0.5초

Dùng ở đâu

Khung không hề di chuyển nên bố cục xung quanh đứng yên tuyệt đối trong lúc chữ tự đi lên.

.mask {
  overflow: hidden;
  max-width: min(380px, 90vw);
}
.mask__text {
  color: #fff7e6;
  animation: mask-up 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes mask-up {
  0%, 4%    { transform: translateY(115%); }
  30%, 78%  { transform: translateY(0); }
  96%, 100% { transform: translateY(115%); }
}

02Nảy từng chữ

Chuyện gì diễn ra trên màn hình

Gắn thuộc tính data-split vào tiêu đề là đủ — một đoạn script ngắn tự tách chữ thành từng thẻ span khi trang vừa mở, gán --i (thứ tự) và --n (nhiễu ngẫu nhiên) cho mỗi chữ, nên chữ nảy lên từ dưới và rơi đúng vị trí lần lượt chứ không cùng lúc, lại hơi lệch nhịp tự nhiên nhờ --n. Trong lúc nảy, từng chữ còn được gỡ mờ dần một lượt trước khi rơi hẳn xuống.

Nảy từng chữ
Mở trong tab mới

02 · Nảy từng chữ — Diện tích đo được 8.615% · khung hình chuyển động 12/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

--i 변수40ms 간격translateY 바운스

Chỗ dùng

Không cần đụng tay vào span nữa — script đọc đúng số chữ hiện có và tự sinh --i, --n tương ứng, nên thêm bớt chữ không phải sửa HTML lẫn keyframes.

.letters span {
  display: inline-block;
  animation: letter-bounce 2s cubic-bezier(.2, 1.4, .4, 1) infinite;
  animation-delay: calc(var(--i) * 40ms);
}
@keyframes letter-bounce {
  0%, 4%    { opacity: 0; transform: translateY(22px); }
  22%, 70%  { opacity: 1; transform: translateY(0); }
  88%, 100% { opacity: 0; transform: translateY(-14px); }
}

03Xoay vào khung hình

Chuyển động thế nào

Mỗi chữ chuyển rotate và opacity cùng lúc từ -25 độ về 0 độ, trông như bay vào lúc còn nghiêng rồi tự đứng thẳng lại. Trong lúc xoay vào, mỗi chữ cũng được gỡ mờ dần, nên vừa đứng thẳng là nét luôn.

Xoay vào khung hình
Mở trong tab mới

03 · Xoay vào khung hình — Diện tích đo được 4.82% · 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

transform: rotateopacity-25deg→0

Dùng khi nào

Vì mỗi chữ tự có delay riêng qua --i, cụm chữ ngắn kiểu badge hay tag xoay vào theo đúng thứ tự đọc.

.rot span {
  animation: rotate-in 300ms ease-out infinite;
  animation-delay: calc(var(--i) * 90ms);
}
@keyframes rotate-in {
  0%, 4%    { opacity: 0; transform: rotate(-25deg); }
  26%, 72%  { opacity: 1; transform: rotate(0deg); }
  90%, 100% { opacity: 0; transform: rotate(18deg); }
}

04Lật chữ 3D

Bạn thấy gì

Bên trong phần tử cha mang perspective, mỗi chữ ẩn mặt sau bằng backface-visibility rồi quay quanh rotateX từ 90 độ về 0 độ, lật ra phía trước như một lá bài nhỏ. Mỗi chữ lật ra còn kèm một lớp gỡ mờ dần, nên khoảnh khắc số hay giá tiền đổi càng rõ mắt hơn.

Lật chữ 3D
Mở trong tab mới

04 · Lật chữ 3D — Diện tích đo được 3.824% · khung hình chuyển động 13/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

perspectiverotateX 90→0backface-visibility

Dùng ở đâu

Cảm giác lật rõ nhất khi con số hay giá tiền vừa đổi, vì mắt dễ bắt được đúng khoảnh khắc chữ đang xoay ngang.

.flip { perspective: 480px; }
.flip span {
  backface-visibility: hidden;
  animation: flip-in 600ms cubic-bezier(.2, .8, .2, 1) infinite;
  animation-delay: calc(var(--i) * 70ms);
}
@keyframes flip-in {
  0%, 4%    { opacity: 0; transform: rotateX(90deg); }
  30%, 74%  { opacity: 1; transform: rotateX(0deg); }
  94%, 100% { opacity: 0; transform: rotateX(-90deg); }
}

05Mờ rồi nét dần

Chuyện gì diễn ra trên màn hình

Trước đây cả câu chữ nét dần cùng lúc như một khối, nhưng giờ mỗi chữ được tách riêng qua data-split và nét dần lệch nhau theo --n — chữ nào rơi vào mốc nhiễu sớm thì nét trước, chữ khác nét sau, nên cảm giác lấy nét trông tự nhiên và ít máy móc hơn hẳn.

Mờ rồi nét dần
Mở trong tab mới

05 · Mờ rồi nét dần — Diện tích đo được 6.333% · 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

filter: bluropacity12px→0

Chỗ dùng

Không một thuộc tính transform nào được động tới, vị trí chữ đứng yên suốt, chỉ độ sắc nét thay đổi.

.blur {
  animation: blur-focus 2s ease-out infinite;
}
@keyframes blur-focus {
  0%, 4%    { opacity: 0; filter: blur(14px); }
  30%, 76%  { opacity: 1; filter: blur(0); }
  96%, 100% { opacity: 0; filter: blur(14px); }
}

06Chữ gợn sóng

Chuyển động thế nào

Mọi chữ chạy chung một đoạn keyframes translateY từ 0 lên -16px rồi về 0, chỉ khác delay 90ms mỗi chữ qua --i, nên riêng độ lệch delay đã đủ khiến cả dòng gợn sóng liên tục. Ở đỉnh sóng, mỗi chữ còn dính một chút filter: blur thoáng qua, giống vệt mờ chuyển động thật.

Chữ gợn sóng
Mở trong tab mới

06 · Chữ gợn sóng — Diện tích đo được 6.247% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

같은 keyframes지연차 파도translateY

Dùng khi nào

Vòng lặp không có điểm vào hay điểm ra, hợp với chữ đứng chờ khi trang đang tải.

.wave span {
  animation: wave-up 2s ease-out infinite;
  animation-delay: calc(var(--i) * 90ms);
}
@keyframes wave-up {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-16px); }
}

07Lắc nhẹ nhấn mạnh

Bạn thấy gì

Cả cụm chữ xoay qua lại rất nhẹ giữa rotate 1-2 độ và translateX 2px, yếu hơn hẳn một cú rung báo lỗi, chỉ đủ kéo mắt liếc qua một lần. 60% đầu vòng lặp đứng yên hoàn toàn, chữ chỉ lắc trong khoảng 66-90%, nên dù xung quanh có chuyển động khác, khoảnh khắc lắc đó vẫn nổi bật.

Lắc nhẹ nhấn mạnh
Mở trong tab mới

07 · Lắc nhẹ nhấn mạnh — Diện tích đo được 4.14% · 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

rotate 1~2degtranslateX 2px짧은 왕복

Dùng ở đâu

Trước khi lắc, mỗi chữ còn chạy một lượt gỡ mờ dần lúc mới xuất hiện, rồi mới tới phần lắc quen thuộc.

.wiggle {
  animation: wiggle 2s ease-out infinite;
}
@keyframes wiggle {
  0%, 60%, 100% { transform: rotate(0) translateX(0); }
  66% { transform: rotate(-2deg) translateX(-2px); }
  72% { transform: rotate(2deg) translateX(2px); }
  78% { transform: rotate(-2deg) translateX(-2px); }
  84% { transform: rotate(2deg) translateX(2px); }
  90% { transform: rotate(0) translateX(0); }
}

08Quét hai tông màu

Chuyện gì diễn ra trên màn hình

Cùng một câu chữ được xếp chồng hai lớp màu khác nhau, còn clip-path của lớp trên quét từ trái sang phải để màu lớp dưới lộ dần ra. Song song với cú quét đó, mỗi chữ còn được gỡ mờ dần ngay lúc xuất hiện.

Quét hai tông màu
Mở trong tab mới

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

clip-path 스윕두 겹 텍스트단색 경계

Chỗ dùng

Ranh giới giữa hai lớp luôn rõ và cứng chứ không mờ dần như gradient, vì clip-path cắt dứt khoát chứ không nội suy màu.

.sweep__top {
  position: absolute;
  inset: 0;
  animation: sweep-wipe 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes sweep-wipe {
  0%, 4%    { clip-path: inset(0 100% 0 0); }
  40%, 62%  { clip-path: inset(0 0 0 0); }
  96%, 100% { clip-path: inset(0 100% 0 0); }
}

09Glitch

Chuyển động thế nào

Cùng một câu chữ chồng hai lớp khác màu bị clip-path cắt thành từng dải mỏng, mỗi lớp lệch translateX vài pixel trong chốc lát rồi trả lại đúng vị trí cũ. Ngoài glitch màu, mỗi chữ còn có một lượt gỡ mờ dần ngắn ngay khi xuất hiện.

Glitch
Mở trong tab mới

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

clip-path 슬라이스translateX 어긋남색 분리

Dùng khi nào

Độ lệch lặp lại hai lượt trong một vòng thay vì chỉ một lần, nên cảm giác màn hình vỡ giống một lỗi thật hơn là một cú rung đơn.

.glitch__r { mix-blend-mode: screen; animation: glitch-r 2s linear infinite; }
@keyframes glitch-r {
  0%   { transform: translateX(0);    clip-path: inset(0 0 0 0); }
  8%   { transform: translateX(-6px); clip-path: inset(12% 0 58% 0); }
  16%  { transform: translateX(4px);  clip-path: inset(48% 0 14% 0); }
  24%  { transform: translateX(0);    clip-path: inset(0 0 0 0); }
  50%  { transform: translateX(-4px); clip-path: inset(20% 0 40% 0); }
  58%  { transform: translateX(5px);  clip-path: inset(55% 0 8% 0); }
  66%  { transform: translateX(0);    clip-path: inset(0 0 0 0); }
}

Chỗ dễ hỏng ở đâu? — bẫy stagger và bẫy màu

Biến --i và calc trên từng chữ

Cả chín mục đều dùng biến --i cùng --n qua calc() để tính độ trễ, đặt phép tính ngay trên animation-delay của từng chữ do script data-split tự sinh ra, không phải trên phần tử cha. Điều đó cho phép thêm bớt chữ mà không phải sửa SCSS lẫn HTML, nhưng đổi lại, nếu bạn gắn thêm một transition opacity trên phần tử cha bọc ngoài, transition đó không hề biết --i hay --n là gì nên toàn bộ chữ mờ dần cùng một nhịp thay vì lệch nhau theo đúng thứ tự đã thấy trong demo. Độ trễ luôn phải nằm trên animation của chính từng chữ.

mix-blend-mode screen và độ sáng

Bẫy thứ hai nằm ở độ sáng. Mục 09 glitch chồng hai lớp đỏ và xanh bằng mix-blend-mode: screen để giả lập màu bị tách ra, nhưng theo trang mix-blend-mode của MDN, screen luôn hoà hai màu về phía trắng, nên nền càng sáng thì hiệu ứng càng bị xoá nhoè. Vì lý do đó mục 09 chỉ đứng trên nền tối, còn muốn dùng trên nền sáng thì phải đổi screen sang difference hoặc một lớp màu đặc có opacity thay vì blend. Đo thật trên khung hình cho thấy mục 06 chuyển động ở 23 trên 24 khung đo được, còn mục 07 giờ chuyển động ở 15 trên 24 khung — tăng hẳn so với 7 khung trước đây, vì lớp gỡ mờ dần theo từng chữ lúc xuất hiện làm nhiều khung hơn có thay đổi thật sự — và chín file đã đóng gói cũng được tinh chỉnh theo đúng sự khác biệt đó — mở bằng mật khẩu 97me82wy gõ đúng như nó hiện trên dòng này.

Khả năng tiếp cận

Trạng thái để lại của từng mục

Cả chín hiệu ứng đều tắt animation khi prefers-reduced-motion: reduce được bật, nhưng trạng thái để lại không giống nhau. Giờ cả chín mục đều chia chữ thành span qua data-split cho lớp gỡ mờ dần mới thêm, nên khi tắt animation thì lớp đó cũng khoá theo — mục 01 và 05 giữ nguyên opacity 1 cùng transform hoặc filter rỗng nên hiện thẳng ra, còn 02, 03, 04, 06 khoá opacity 1 và transform none cho từng chữ cùng lúc, không còn thứ tự lệch nhau. Mục 07 và 09 chỉ đóng vai trang trí thuần tuý nên có thể tắt hẳn không để lại dấu vết, riêng mục 08 giữ nguyên clip-path ở trạng thái đã quét hết vì màu chính là thông tin cần đọc được ngay. Mức hỗ trợ trình duyệt của media query này được liệt kê trên trang prefers-reduced-motion của MDN.

@media (prefers-reduced-motion: reduce) {
  .mask__text, .blur, .wiggle { animation: none; transform: none; filter: none; opacity: 1; }
  .letters span, .rot span, .flip span, .wave span { animation: none; opacity: 1; transform: none; }
  .sweep__top { animation: none; clip-path: inset(0 0 0 0); }
  .glitch__r, .glitch__b { animation: none; opacity: 0; }
}

Các hiệu ứng chữ khác nằm chung trong chuyên mục CSS, còn trang giới thiệu nói rõ site này đóng gói mỗi bài thành file zip có khoá như thế nào.

FAQ

Chỉ cần một hiệu ứng, không lấy hết chín cái, có được không?

Được. Mỗi hiệu ứng gói gọn trong một class riêng và không chia sẻ biến hay keyframes với mục khác, nên copy đúng phần SCSS cùng đoạn HTML đi kèm là chạy độc lập ngay. Mục 01 và 05 vẫn là hai bản dễ đọc nhất nếu bạn chỉ cần một chuyển động — chuyển động chính vẫn nằm trên cả khối, chỉ riêng lớp gỡ mờ dần mới cần chia chữ qua data-split.

Đổi tốc độ hoặc thời lượng animation ở đâu?

Mỗi file SCSS khai một biến $duration hoặc thời lượng ngay trong khai báo animation ở đầu file, nên chỉ cần sửa một con số đó là toàn bộ nhịp đổi theo. Cả chín mục giờ đều dùng --i (và --n cho lớp gỡ mờ dần) nên còn có thêm một khoảng trễ mỗi chữ (40ms, 70ms, 90ms tuỳ mục) khai riêng ngay cạnh, tách hẳn khỏi thời lượng animation.

Dùng chung với Tailwind hoặc dự án React được không?

Được. Bản demo biên dịch từ SCSS ra CSS thuần nên không kéo theo công cụ build nào, còn một rule @keyframes tuỳ biến như thế này vẫn khai animation ngoài bộ utility mặc định của Tailwind, vì hiệu ứng chữ dạng này hiếm khi có sẵn utility dựng sẵn tương ứng. Thư mục react/ trong zip có sẵn một component riêng cho mỗi hiệu ứng, nhận số chữ và thời lượng qua props.

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.