GODRICH

9 CSS Text Highlight Animation: dán 1 dòng là chạy

CSS text highlight animation là hiệu ứng CSS thuần giúp tô sáng, gạch chân hoặc gạch ngang một từ trong câu ngay khi trang tải, không cần ảnh hay JavaScript.

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

Chín hiệu ứng dưới đây được xếp theo tình huống cần nhấn mạnh trong câu, không theo mức phổ biến. 01 đến 03 tô đậm một cụm từ then chốt, 04 và 05 đánh dấu cảnh báo hoặc việc đã xong, 06 và 07 dẫn mắt theo liên kết và từng bước, còn 08 và 09 làm badge và con số bật lên. Ở lưới phía trên, chín ô tự lặp lại bên trong một khung iframe cố định vì cả chín đều là animation chạy sẵn, không chờ hover hay JS nào cả. Mỗi mục dưới đây chỉ giữ phần code cốt lõi, bản đầy đủ đã đo khung hình thật nằm trong file zip.

01Marker tô sáng

Một khối ::before màu vàng nằm sau từ khoá, mọc rộng từ trái sang phải qua transform: scaleX(0 → 1) rồi mix-blend-mode: multiply đặt nó lên trên chữ như vừa tô marker xong. Dùng cho cụm từ then chốt trong điều khoản, thông báo hay từ nhấn mạnh trong bài hướng dẫn.

transform:scaleXmix-blend-mode2s
.hl {
  position: relative;
  isolation: isolate;
  &::before {
    content: ""; position: absolute; inset: 12% -2% 6%;
    background: $color; border-radius: $r-xs;
    transform: scaleX(0); transform-origin: left center;
    mix-blend-mode: multiply; z-index: -1;
    animation: mark $duration $easing infinite;
  }
}
@keyframes mark {
  0%   { transform: scaleX(0); }
  20%  { transform: scaleX(1); }
  75%  { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

02Marker chồng lớp

Cùng một khối marker được nhân đôi thành ::before và ::after, mỗi lớp xoay một góc rotate riêng và lớp sau vào trễ .3s ngay trong shorthand animation khi scaleX chạy, nên hai nét chồng lệch như tay tô hai lần. Dùng cho từ quan trọng nhất trên trang hay từ khoá đáp án trong FAQ.

::before·::afterrotate + scaleX지연 0/.3s
.hl::before, .hl::after {
  content: ""; position: absolute; inset: 10% -2% 4%;
  transform: scaleX(0); transform-origin: left center;
  mix-blend-mode: multiply; z-index: -1;
}
.hl::before { background: $stage-yellow; rotate: -1.2deg; animation: mark $duration $easing infinite; }
.hl::after  { background: $color; opacity: .55; rotate: 1deg; animation: mark $duration $easing infinite .3s; }

03Gạch chân viết tay

Một path SVG cong hơi lệch được vẽ bằng stroke-dasharray đúng bằng chiều dài path, còn stroke-dashoffset chạy từ giá trị đó về 0 rồi về âm, khiến gạch chân hiện ra rồi biến mất như bút vừa lướt qua. Dùng cho từ khoá trong trích dẫn bài blog hay từ nhấn mạnh trên trang giới thiệu bản thân.

SVG pathstroke-dashoffset600ms
.hl__path {
  fill: none; stroke: $color; stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: 140; stroke-dashoffset: 140;
  animation: draw $duration $easing infinite;
}
@keyframes draw {
  0%   { stroke-dashoffset: 140; }
  35%  { stroke-dashoffset: 0; }
  80%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -140; }
}

04Gạch chân dạng sóng

text-decoration-style: wavy vẽ sẵn gạch chân gợn sóng có trong trình duyệt, còn một custom property kiểu đăng ký qua @property được đưa vào text-underline-offset để dao động giữa 0px và 5px như đang thở. Dùng cho nhấn mạnh kiểu kiểm tra chính tả hay câu cảnh báo cần thu hút chú ý. Cả text-decoration-thickness lẫn text-underline-offset đã ổn định từ Chrome/Edge 89+, Safari 15+ và Firefox 70+ theo trang text-underline-offset của MDN.

text-decoration-style:wavy@property <length>1s
@property --underline-offset {
  syntax: "<length>"; inherits: true; initial-value: 0px;
}
.hl {
  text-decoration-line: underline;
  text-decoration-style: wavy;
  text-decoration-color: $color;
  text-underline-offset: var(--underline-offset);
  animation: wave $duration linear infinite;
}
@keyframes wave {
  0%, 100% { --underline-offset: 0px; }
  50%      { --underline-offset: 5px; }
}

05Gạch ngang vẽ dần

Một thanh .price__strike riêng nằm giữa chiều cao chữ mọc rộng qua transform: scaleX(0 → 1) với transform-origin: left, dựng lại đúng khoảnh khắc một mức giá bị gạch ngang trước mắt. Dùng cho hiển thị giá trước khi giảm hay mục việc-cần-làm đã hoàn tất.

transform-origin: lefttransform:scaleX500ms
.price__strike {
  position: absolute; left: -2%; right: -2%; top: 52%; height: 2px;
  background: $color;
  transform: scaleX(0); transform-origin: left center;
  animation: strike $duration $easing infinite;
}
@keyframes strike {
  0%   { transform: scaleX(0); }
  30%  { transform: scaleX(1); }
  80%  { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

06Tô nền lấp đầy

linear-gradient phủ nền liên kết và background-size chạy từ 0% 100% lên 100% 100% để màu lấp đầy từ trái sang, còn background-position: left giữ cố định để phần đã tô không trôi ngược lại. Dùng cho liên kết trong menu điều hướng hay nút dạng văn bản trong card.

background-sizebackground-position:left2s
.nav {
  background: linear-gradient(90deg, $color, $color);
  background-repeat: no-repeat;
  background-position: left center;
  background-size: 0% 100%;
  animation: fill $duration $easing infinite;
}
@keyframes fill {
  0%   { background-size: 0% 100%; }
  25%  { background-size: 100% 100%; }
  75%  { background-size: 100% 100%; }
  100% { background-size: 0% 100%; }
}

07Đường chấm mọc dần

Một thanh mảnh vẽ chấm cách đều bằng repeating-linear-gradient nằm dưới chữ và mọc rộng qua transform: scaleX(0 → 1), nên đường chấm trông như đang nối thêm từng chấm một từ trái. Dùng cho đường dẫn tiến độ trong hướng dẫn từng bước hay đường nối chú thích.

repeating-linear-gradienttransform:scaleX700ms
.hl__dots {
  background-image: repeating-linear-gradient(90deg, $color 0 3px, transparent 3px 8px);
  transform: scaleX(0); transform-origin: left center;
  animation: grow $duration $easing infinite;
}
@keyframes grow {
  0%   { transform: scaleX(0); }
  40%  { transform: scaleX(1); }
  80%  { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

08Dải màu quét qua

Một dải gradient chéo rộng 250% nằm phía sau badge, background-position trượt liên tục từ 150% xuống -150% theo linear infinite và mix-blend-mode: overlay hoà vào nền badge, tạo một vệt sáng quét qua không dừng. Dùng cho badge khuyến mãi hay nhãn mới, giới hạn.

대각선 그러데이션background-position2s·linear
.badge::after {
  background: linear-gradient(115deg, transparent 30%, $color 48%, transparent 66%);
  background-size: 250% 100%;
  mix-blend-mode: overlay;
  animation: sweep $duration linear infinite;
}
@keyframes sweep {
  0%   { background-position: 150% 0; }
  100% { background-position: -150% 0; }
}

09Bóng chữ bật lên

Một custom property kiểu đăng ký qua @property điều khiển bán kính mờ của filter: drop-shadow() chạy từ 0 lên 10px, đồng thời transform: scale(1 → 1.06 → 1) làm chữ nảy nhẹ một nhịp. Dùng cho giá cuối trên bảng giá hay số đếm ngược cần nhấn mạnh.

@property <length>filter:drop-shadow1s
@property --shadow-blur {
  syntax: "<length>"; inherits: false; initial-value: 0px;
}
.hl {
  filter: drop-shadow(0 0 var(--shadow-blur) $color);
  animation: pop $duration $easing infinite;
}
@keyframes pop {
  0%   { transform: scale(1);    --shadow-blur: 0px; }
  40%  { transform: scale(1.06); --shadow-blur: 10px; }
  100% { transform: scale(1);    --shadow-blur: 0px; }
}

Bẫy nằm ở đâu — isolation và biên độ quá hẹp

Hai trong chín hiệu ứng — 01 marker tô sáng và 02 marker chồng lớp — hoàn toàn vô hình lúc mới dựng xong. Khối marker bị đẩy xuống dưới chữ bằng z-index: -1, nhưng nếu phần tử mang class .hl không có isolation: isolate, trình duyệt vẽ khối đó phía sau lớp nền của tổ tiên gần nhất không định vị (position), trước cả khi chạm tới ::before, nên toàn bộ khối màu bị nền sân khấu che kín dù CSS không báo lỗi gì. Thêm đúng một dòng isolation: isolate là đủ, và số liệu đo khung hình xác nhận điều đó: diện tích tích luỹ nhảy từ 0% lên 0.81% ở mục 01 và 1.12% ở mục 02.

Mục 04 vướng một bẫy khác hẳn. Bản dựng đầu tiên cho text-underline-offset dao động chỉ giữa 1px và 3px, biên độ đó nhỏ hơn ngưỡng đổi màu pixel giữa hai khung liên tiếp, nên phép đo chỉ bắt được 4 trên 23 khung có chuyển động thật, không đạt mức tối thiểu để tính là một animation. Nới biên độ ra 0px đến 5px và đổi timing sang linear, đưa con số đó lên 23/23 — giá trị càng nhỏ trông càng tinh tế, nhưng tinh tế và vô hình chỉ cách nhau một chút, và không đo khung hình thật thì khó biết ranh giới đó nằm ở đâu. Bản đã sửa cả hai chỗ này nằm sẵn trong file nén, mở bằng mật khẩu hx9yh6mr, gõ đúng như nó xuất hiện trên dòng này.

Khả năng tiếp cận

Người bật giảm chuyển động trên hệ điều hành nhận được chín kết quả khác nhau chứ không phải một quy tắc chung cho tất cả. 01 và 02 giữ khối marker ở scaleX(1), coi như đã tô xong và đứng yên tại đó. 03 khoá stroke-dashoffset về 0, nên gạch chân viết tay hiện trọn vẹn ngay từ đầu. 04 cố định text-underline-offset ở 2px, gần giữa khoảng dao động cũ. 05 và 07 cũng dừng ở scaleX(1), đường gạch ngang và đường chấm coi như đã vẽ xong sẵn. 06 giữ background-size: 100% 100%, nền coi như đã tô đầy từ đầu. 08 chuyển hẳn lớp quét sang opacity: 0, gỡ vệt sáng khỏi badge. 09 giữ drop-shadow ở 4px nhưng bỏ phần transform: scale, nên bóng còn mà cú nảy biến mất. Ba mục có thêm lớp trang trí riêng — svg ở 03, đường kẻ ở 05, thanh chấm ở 07 — đều gắn aria-hidden="true" để trình đọc màn hình bỏ qua lớp đó và đọc thẳng câu chữ thật bên dưới. Từng mốc phiên bản trình duyệt bắt đầu hỗ trợ prefers-reduced-motion đều được liệt kê đầy đủ trong tài liệu MDN về prefers-reduced-motion.

Những kiểu tô chữ khác ngoài chín cái này được gom trong danh mục CSS, còn ai đứng sau site thì xem ở trang giới thiệu.

FAQ

Có thể dùng cả chín kiểu trên cùng một trang không?

Được, nhưng mỗi câu chỉ nên mang một kiểu tô. Marker (01, 02) và gạch chân sóng (04) chồng lên cùng một cụm từ sẽ khiến mắt không biết nhìn vào đâu trước. Gạch ngang (05) và badge quét màu (08) thuộc ngữ cảnh khác hẳn nên đứng chung trang vẫn tự nhiên.

Đổi cụm từ được tô sáng bằng cách nào?

Chỉ cần bọc đúng phần chữ cần nhấn trong một span mang class .hl, phần còn lại của đoạn văn giữ nguyên thẻ p bình thường. Bản React trong thư mục react/ của file zip nhận chữ cần tô qua một prop kiểu children hoặc text, nên nội dung động lấy từ server vẫn chạy đúng.

mix-blend-mode: multiply ở mục 01, 02 có vỡ trên nền tối không?

Multiply nhân giá trị màu của lớp trên với lớp dưới, nên trên nền sáng nó chỉ làm màu marker trầm lại một chút rất tự nhiên. Trên nền tối, phép nhân đó kéo cả marker gần về đen và gần như biến mất, vì vậy bản dark mode nên đổi mix-blend-mode sang screen hoặc bỏ hẳn blend.

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.