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

9 CSS Marquee Infinite Loop — chỉ CSS, không JS

CSS marquee infinite loop là kỹ thuật giúp một dải nội dung trôi liên tục, không lộ đường nối, dựng hoàn toàn bằng CSS và không cần một dòng JS nào.

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

Chín demo dưới đây không xếp theo độ nổi bật mà theo lớp kỹ thuật chồng dần lên nhau: công thức không lộ đường nối (01) được đưa thẳng vào một cách bố trí thực tế, hay được dùng (02), rồi cộng thêm tương tác (03) và làm mịn thị giác (04), sau đó đổi trục và nhân đôi hướng chạy (05–06), khép lại bằng nhịp độ và chiều sâu (07–08) trước khi dừng ở khả năng tiếp cận (09). Khung iframe cố định phía trên không nhận sự kiện cuộn hay hover thật nên mỗi ô tự lặp lại một chu kỳ hai giây bằng keyframes riêng của nó — trên một trang thật, một vòng thường mất từ mười đến hai mươi giây, và tên logo hay dòng chữ trong demo chỉ là chỗ để bạn thay bằng nội dung thật.

01Công thức loop liền mạch

Bạn thấy gì

Sáu chip được nhân đôi thành mười hai và xếp chung một track flex, mỗi chip mang margin-right cố định thay vì gap của track, rồi transform chỉ dịch từ 0 đến đúng -50% — nhờ vậy bề rộng của một bộ sáu chip đúng bằng nửa track, bản sao rơi đúng vào chỗ bản gốc từng đứng mà không lệch một pixel nào.

Công thức loop liền mạch
Mở trong tab mới

01 · Công thức loop liền mạch — Diện tích đo được 8.889% · 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

translateX(-50%)콘텐츠 2벌@keyframes

Dùng ở đâu

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 12s linear infinite;
}
.marquee__chip {
  margin-right: 14px;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

02Marquee logo vô hạn

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

Năm wordmark đối tác được xếp thành thẻ viên thuốc rồi chảy qua đúng track liền mạch của mục 01, còn năm thẻ nhân bản phía sau mang aria-hidden="true" để trình đọc màn hình chỉ đọc từng tên đối tác một lần.

Marquee logo vô hạn
Mở trong tab mới

02 · Marquee logo vô hạn — Diện tích đo được 8.028% · 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

로고 카드gap 고정폭linear infinite

Chỗ dùng

.marquee__track {
  display: flex;
  animation: logo-scroll 12s linear infinite;
}
.marquee__logo {
  margin-right: 16px;
  height: 34px;
  padding: 0 16px;
  border-radius: 999px;
  background: #fff;
}

03Dừng khi hover

Chuyển động thế nào

Ngay khi con trỏ chạm vào banner, animation-play-state chuyển sang paused và giữ nguyên đúng khung hình đang chạy, không giật ngược về đầu vòng lặp như nhiều bản tự viết vẫn mắc phải.

Dừng khi hover
Mở trong tab mới

03 · Dừng khi hover — Diện tích đo được 3.244% · khung hình chuyển động 19/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

animation-play-statepausedhover

Dùng khi nào

.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
  animation-play-state: paused;
}
@keyframes marquee-hover-cycle {
  0%   { transform: translateX(0); }
  38%  { transform: translateX(-32%); }
  62%  { transform: translateX(-32%); }
  100% { transform: translateX(-50%); }
}

04Làm mờ hai đầu bằng mask

Bạn thấy gì

Một linear-gradient được gán vào mask-image, kèm bản có tiền tố -webkit-mask-image cho Safari, phủ lên hai đầu track khiến từng thẻ mờ dần vào nền thay vì bị cắt cụt như khi chỉ dùng overflow: hidden.

Làm mờ hai đầu bằng mask
Mở trong tab mới

04 · Làm mờ hai đầu bằng mask — Diện tích đo được 10.501% · 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

mask-imagelinear-gradient-webkit-mask

Dùng ở đâu

.marquee {
  overflow: hidden;
  mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
  -webkit-mask-image: linear-gradient(to right,
    transparent, #000 14%, #000 86%, transparent);
}

05Ticker chạy dọc

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

Đúng công thức nhân đôi của mục 01 được chuyển sang trục dọc — translateY thay cho translateX — để một dòng gồm tên và giá cuộn lên bên trong một khung có chiều cao cố định.

Ticker chạy dọc
Mở trong tab mới

05 · Ticker chạy dọc — Diện tích đo được 9.767% · 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

translateY(-50%)column 트랙고정 높이

Chỗ dùng

.ticker__track {
  display: flex;
  flex-direction: column;
  animation: ticker-scroll 12s linear infinite;
}
@keyframes ticker-scroll {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

06Hai dòng chạy ngược chiều

Chuyển động thế nào

Hai track dùng chung một animation-name và cùng thời lượng, chỉ hàng phía dưới nhận thêm animation-direction: reverse nên hai dòng chip trôi ngược chiều nhau mà không cần viết thêm một bộ keyframes nào khác.

Hai dòng chạy ngược chiều
Mở trong tab mới

06 · Hai dòng chạy ngược chiều — Diện tích đo được 14.222% · 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

animation-direction: reverse두 트랙교차 흐름

Dùng khi nào

.marquee__track {
  animation-name: marquee-scroll;
  animation-duration: 12s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.marquee__row--rev .marquee__track {
  animation-direction: reverse;
}

07Nhịp độ nhanh chậm

Bạn thấy gì

Thay timing-function linear bằng cubic-bezier(.65,0,.35,1) khiến vòng lặp chậm lại ngay tại đường nối rồi nhanh hơn ở giữa quãng đường, trong khi quãng dịch chuyển vẫn giữ nguyên đúng -50% như công thức gốc.

Nhịp độ nhanh chậm
Mở trong tab mới

07 · Nhịp độ nhanh chậm — Diện tích đo được 5.417% · 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

cubic-bezier비선형 easing이음매 유지

Dùng ở đâu

.marquee__track {
  animation: marquee-scroll 9s
    cubic-bezier(.65, 0, .35, 1) infinite;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

08Marquee cong 3D

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

Stage nhận perspective còn track nhận preserve-3d, rồi mỗi thẻ trong năm thẻ được gán một bước rotateY riêng qua nth-child để cả dải uốn cong thành mặt trong của một hình trụ.

Marquee cong 3D
Mở trong tab mới

08 · Marquee cong 3D — Diện tích đo được 13.333% · 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

perspectivepreserve-3d단계별 rotateY

Chỗ dùng

.stage { perspective: 420px; }
.marquee__track { transform-style: preserve-3d; }
.marquee__card:nth-child(1),
.marquee__card:nth-child(6) {
  transform: rotateY(-44deg) translateZ(-40px);
}
.marquee__card:nth-child(3),
.marquee__card:nth-child(8) {
  transform: rotateY(0deg) translateZ(0px);
}

09Bản tĩnh khi giảm chuyển động

Chuyển động thế nào

Công tắc trên demo cho xem song song track đang chạy và trạng thái tĩnh mà prefers-reduced-motion: reduce thực sự hiển thị cho người dùng thật, dừng đúng khung hình đang ở giữa vòng lặp chứ không phải khung đầu tiên.

Bản tĩnh khi giảm chuyển động
Mở trong tab mới

09 · Bản tĩnh khi giảm chuyển động — Diện tích đo được 5.672% · 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

prefers-reduced-motion정적 목록토글 비교

Dùng khi nào

@media (prefers-reduced-motion: reduce) {
  .marquee__track {
    animation: none !important;
    transform: translateX(-30%);
  }
  .panel__toggle-knob {
    transform: translateX(16px);
  }
}

Bẫy nằm ở đâu? — hai chỗ dễ hỏng

Bản nhân đôi và trình đọc màn hình

Lần đầu dựng mục 02, năm thẻ logo nhân bản không được gắn thuộc tính ẩn khỏi trình đọc màn hình. Bật thử một trình đọc màn hình thì mười cái tên được đọc hết, tưởng như có mười đối tác trong khi thực chỉ có năm. Trang tham chiếu aria-hidden của MDN nói rõ thuộc tính này chỉ giấu phần tử khỏi công nghệ hỗ trợ chứ không giấu khỏi màn hình, nên chỉ cần gắn aria-hidden="true" cho từng span nhân bản là giọng đọc dừng lại đúng ở năm cái tên gốc.

Khoảng gap và đường nối

Bản dựng đầu của mục 06 lại vấp bẫy khác, và bẫy này lặp lại ở tám trong chín mục: mỗi mục nhân đôi bộ chip gốc rồi xếp chung một track có gap, nhưng vì gap chỉ nằm giữa hai chip liền kề nên tổng số khoảng gap luôn ít hơn tổng số chip đúng một, khiến translateX(-50%) hụt mất nửa gap và mỗi vòng lặp giật một cái rất ngắn ngay tại đường nối — đo trên Chrome, mục 06 giật 5,8px, còn cả tám mục dao động 4–20px tuỳ độ rộng gap của từng mục. Đổi khoảng cách đó từ gap của track sang margin-right trên chính từng chip — bề rộng một bộ chip khi đó đúng bằng nửa track theo pixel — đưa đường nối của cả tám mục xuống còn 0–0,2px, coi như không còn giật.

Chín file đã được canh lại đường nối và sửa cả hai lỗi trên, nằm gọn trong một file nén, mở bằng mật khẩu gcjydr57, gõ đúng như nó hiện ở đây thì file sẽ mở.

Khả năng tiếp cận

Giảm chuyển động và điều hướng bàn phím

Cả chín demo đều tắt animation dưới prefers-reduced-motion: reduce và giữ track đứng yên ở vị trí cuối thay vì biến mất khỏi màn hình. Mục 03 còn giữ nguyên trạng thái dừng đó khi bàn phím điều hướng tới banner qua focus-within, nên người dùng Tab qua không bao giờ bỏ lỡ dòng chữ đang đọc dở. Mục 09 đi xa hơn một bước — nó dựng sẵn cả hai trạng thái cạnh nhau trên cùng một công tắc để bạn nhìn trực tiếp sự khác biệt thay vì phải đổi cài đặt hệ điều hành rồi tải lại trang mới thấy. Ticker dọc ở mục 05 tuân theo đúng quy tắc đó, không có ngoại lệ nào cho trục dọc. Trang prefers-reduced-motion của MDN liệt kê hai giá trị của media feature này cùng mức hỗ trợ theo từng trình duyệt.

Nếu bạn muốn xem thêm demo chạy theo cuộn trang, mục lục nằm trong danh mục CSS, còn lý do trang này chọn đúng chín kiểu marquee để viết nằm ở trang giới thiệu.

FAQ

Có bắt buộc phải nhân đôi nội dung thành hai bản y hệt không?

Có, với đúng công thức -50% dùng ở tám trong chín mục. Vòng lặp chỉ liền mạch khi bản sao là bản sao chính xác của bản gốc, kể cả khoảng cách margin-right giữa các phần tử; thiếu một item hay lệch một khoảng cách thì đường nối lộ ra ngay ở khung hình cuối mỗi vòng.

Chín marquee này có xung đột với một trang dùng Tailwind không?

Không. Chín file biên dịch từ SCSS ra CSS thuần, không đụng vào cấu hình build nào cả, nên display: flex, gap và @keyframes map thẳng sang các utility tương ứng của Tailwind. Chỉ riêng mục 08 cần khai báo perspective và transform-style: preserve-3d ngoài bộ utility mặc định vì đó là thuộc tính 3D mà Tailwind ít khi có sẵn utility class riêng.

Marquee chạy bằng CSS này có nặng hơn một thư viện marquee viết bằng JS không?

Nhẹ hơn. Cả chín mục chỉ animate transform, thuộc tính mà trình duyệt xử lý ở bước hợp thành mà không phải tính lại layout mỗi khung hình, còn phần lớn thư viện JS phải đo lại chiều rộng nội dung bằng tay ở mỗi lần render. Cái giá phải trả là bạn tự chịu trách nhiệm giữ hai bản nội dung khớp nhau tuyệt đối, việc mà một thư viện JS thường tự động hoá.

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.