Chữ chạy (marquee) là hiệu ứng trực quan trong đó văn bản tự động cuộn theo chiều ngang trong một khu vực giới hạn, cho phép hiển thị các chuỗi dài trong một vùng chứa nhỏ gọn. Về mặt lịch sử, thuật ngữ này bắt nguồn từ thẻ HTML <marquee>, phổ biến trên web vào những năm 1990. Trong phát triển di động hiện đại, scrolling text được sử dụng trong trình phát, băng tin và thanh trạng thái. Theo Apple Human Interface Guidelines và Google Material Design, marquee chỉ được chấp nhận trong các kịch bản hạn chế khi việc cắt tĩnh bằng dấu ba chấm làm giảm trải nghiệm người dùng.
Các điểm chính
Chữ chạy là hiệu ứng giao diện người dùng trong đó văn bản di chuyển theo chiều ngang trong một khu vực cố định, cuộn theo chu kỳ để người dùng có thể đọc toàn bộ nội dung không vừa trong vùng chứa. Không giống như ellipsize, ẩn một phần văn bản, marquee hiển thị toàn bộ văn bản nhưng đang chuyển động.
Thuật ngữ marquee được thiết lập nhờ thẻ HTML <marquee>, được thêm vào trong các phiên bản đầu của trình duyệt Netscape và Internet Explorer. Thẻ này cho phép tạo văn bản cuộn mà không cần JavaScript hay CSS, nhưng chưa bao giờ là một phần của tiêu chuẩn HTML. Các trình duyệt hiện đại tiếp tục hỗ trợ nó để tương thích ngược, nhưng không khuyến khích sử dụng.
Trong phát triển di động, marquee giải quyết vấn đề nhãn dài trong vùng chứa nhỏ gọn: tên bản nhạc trong trình phát, bảng giá tiền tệ, băng tin trên màn hình TV. Tuy nhiên, do hiệu ứng, nó tạo ra gánh nặng cho khả năng tiếp cận và có thể gây khó chịu cho người dùng — vì vậy nó được sử dụng có chọn lọc.
Theo nghiên cứu của Nielsen Norman Group (2023), văn bản chuyển động làm giảm tốc độ đọc 15-20% và tăng tải nhận thức. Chỉ sử dụng marquee khi việc cắt tĩnh (ellipsize) thực sự không thể chấp nhận được.
Trên Android, marquee được triển khai thông qua cơ chế tích hợp của TextView qua thuộc tính android:ellipsize="marquee". Đây là chế độ ellipsize duy nhất không cắt văn bản mà tạo hiệu ứng cuộn.
Để kích hoạt marquee, cần đáp ứng ba điều kiện: singleLine="true" (hoặc maxLines="1"), ellipsize="marquee" và setSelected(true) trong mã. Nếu không gọi setSelected(), hiệu ứng sẽ không bắt đầu.
<!-- XML layout -->
<TextView
android:id="@+id/marquee_text"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever" />
// Kotlin: activate marquee
textView.setSelected(true)
textView.isSingleLine = true
Thuộc tính marqueeRepeatLimit xác định số lần lặp lại hiệu ứng: marquee_forever (vô hạn) hoặc một số cụ thể. Giá trị mặc định là marquee_forever. Nếu văn bản ngắn hơn chiều rộng vùng chứa, hiệu ứng sẽ không bắt đầu — TextView hiển thị văn bản tĩnh.
Một đặc điểm quan trọng: setSelected(true) chỉ hoạt động khi TextView có tiêu điểm hoặc ở chế độ được chọn. Trong RecyclerView, điều này có thể gây ra sự cố — hiệu ứng chỉ bắt đầu cho một mục. Giải pháp: tạo MarqueeTextView tùy chỉnh ghi đè isFocused() và luôn trả về true.
Bắt đầu từ Android 9 (API 28), Google đã cải thiện hành vi của marquee — hiệu ứng trở nên mượt mà hơn và không bị đóng băng khi cuộn danh sách. Tuy nhiên, trên các thiết bị cũ, marquee có thể bị giật, đặc biệt ở mật độ điểm ảnh cao.
iOS không có tương đương tích hợp cho android:ellipsize="marquee". Marquee được triển khai thủ công bằng Core Animation hoặc hiệu ứng SwiftUI. Cách tiếp cận phổ biến nhất là tạo hiệu ứng cho layer.position kết hợp với vùng chứa cắt.
Trong UIKit, marquee được xây dựng từ ba phần tử: vùng chứa UILabel với clipsToBounds = true, văn bản và CABasicAnimation để dịch chuyển văn bản theo trục X với lặp vô hạn và autoreverses = false, để dòng cuộn theo một hướng và nhảy trở lại.
func startMarquee(for label: UILabel) {
label.clipsToBounds = true
label.numberOfLines = 1
let textWidth = (label.text as? NSString)?
.size(withAttributes: [.font: label.font as any]).width ?? 0
guard textWidth > label.bounds.width else { return }
let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = label.layer.position.x + label.bounds.width / 2
animation.toValue = label.layer.position.x - textWidth - label.bounds.width / 2
animation.duration = CFTimeInterval(textWidth / 60)
animation.repeatCount = .infinity
animation.autoreverses = false
label.layer.add(animation, forKey: "marquee")
}
Tính toán duration = textWidth / 60 cho tốc độ khoảng 60 pixel mỗi giây — tốc độ đọc thoải mái. Nếu tốc độ vượt quá 120 px/s, văn bản trở nên không đọc được. Nếu dưới 30 px/s, hiệu ứng trông chậm chạp. Điều chỉnh tốc độ cho kịch bản cụ thể của bạn.
Trong SwiftUI, marquee được triển khai đơn giản hơn — qua bộ điều chỉnh offset() với hiệu ứng lặp và .animation(.linear(duration:).repeatForever(autoreverses: false)). SwiftUI tự động quản lý vòng đời hiệu ứng nhưng yêu cầu GeometryReader để xác định chiều rộng văn bản.
Trên web, thẻ <marquee> lỗi thời không được khuyến khích sử dụng, mặc dù trình duyệt vẫn hỗ trợ nó. Triển khai hiện đại của marquee là CSS animation với keyframes và transform: translateX.
Ưu điểm của cách tiếp cận CSS: kiểm soát hoàn toàn tốc độ, tạm dừng và đường cong hiệu ứng, hỗ trợ prefers-reduced-motion cho khả năng tiếp cận và không có thẻ không đúng ngữ nghĩa.
.marquee {
overflow: hidden;
white-space: nowrap;
width: 300px;
}
.marquee-content {
display: inline-block;
animation: marquee 10s linear infinite;
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
/* Pause on hover */
.marquee-content:hover {
animation-play-state: paused;
}
Mẹo chính: display: inline-block trên nội dung đảm bảo chiều rộng của phần tử bằng chiều rộng văn bản và translateX(-100%) dịch chuyển nó chính xác bằng chiều rộng đó sang trái. Nếu nội dung là display: block, translateX(-100%) sẽ bằng chiều rộng vùng chứa, không phải chiều rộng văn bản, và hiệu ứng sẽ hỏng.
Để hỗ trợ tiếp cận, thêm @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } } — người dùng bị rối loạn tiền đình sẽ thấy văn bản tĩnh với khả năng cuộn.
Marquee được biện minh trong các kịch bản mà người dùng không thể tự mở rộng vùng chứa và toàn bộ văn bản rất quan trọng: bản nhạc hiện tại trong trình phát trên màn hình khóa, băng tin trên bảng TV hoặc marquee trong ứng dụng gọi xe (trạng thái đơn hàng).
Marquee không nên được sử dụng trong bảng, danh sách, biểu mẫu và hộp thoại — bất cứ nơi nào người dùng tương tác tích cực với giao diện. Hiệu ứng làm xao nhãng khỏi mục tiêu, tăng thời gian tìm kiếm phần tử và tạo ra sự khó chịu. Theo WebAIM (2024), 1 trong 3 người dùng bị rối loạn tiền đình báo cáo sự khó chịu khi xem văn bản hoạt hình.
Nguyên tắc chung: nếu văn bản có thể được rút ngắn hoặc xuống dòng tiếp theo — hãy làm điều đó. Marquee là giải pháp cuối cùng, không phải giải pháp phổ quát cho nhãn dài.
Marquee là một trong những hiệu ứng UI có vấn đề nhất từ góc độ khả năng tiếp cận. Văn bản chuyển động có thể gây chóng mặt, buồn nôn và mất phương hướng ở những người bị rối loạn tiền đình. Theo WCAG 2.2, bất kỳ hiệu ứng nào kéo dài hơn 5 giây phải có cơ chế tạm dừng hoặc tắt.
Trên iOS, sử dụng thuộc tính UIAccessibility.isReduceMotionEnabled để kiểm tra cài đặt hệ thống. Nếu reduceMotion được bật — không bắt đầu hiệu ứng, hiển thị văn bản tĩnh với ellipsize.
Trên Android, kiểm tra hiệu ứng hệ thống qua Settings.Global.ANIMATOR_DURATION_SCALE. Nếu hiệu ứng toàn cục bị tắt (duration scale = 0), không nên bật marquee.
Trên web, sử dụng tính năng đa phương tiện prefers-reduced-motion: @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } }. Đây là tính năng CSS tiêu chuẩn được hỗ trợ bởi tất cả các trình duyệt hiện đại. Kiểm tra trong DevTools: bật mô phỏng prefers-reduced-motion và xác nhận rằng hiệu ứng tắt.
Câu hỏi thường gặp
Ellipsize cắt văn bản và thêm dấu ba chấm, ẩn một phần nội dung. Marquee cuộn toàn bộ văn bản nhưng đang chuyển động. Lựa chọn phụ thuộc vào ưu tiên: hiển thị mọi thứ (marquee) hoặc duy trì hiển thị tĩnh (ellipsize).
android:ellipsize="marquee" tích hợp không cho phép thay đổi tốc độ — nó được xác định bởi ScrollSpeed hệ thống. Để có tốc độ tùy chỉnh, hãy sử dụng Animation hoặc ValueAnimator thủ công.
Do tái sử dụng view, setSelected(true) bị đặt lại. Giải pháp: tạo MarqueeTextView tùy chỉnh ghi đè isFocused() và trả về true, hoặc sử dụng hiệu ứng tùy chỉnh trong Adapter.onViewAttachedToWindow().
Có, bằng cách sử dụng translateY thay vì translateX. Marquee dọc được sử dụng cho credits hoặc băng tin trong luồng dọc. Nguyên tắc giống nhau, nhưng trục hiệu ứng thay đổi.
Trong CSS: :hover { animation-play-state: paused; }. Trên Android: theo dõi tiêu điểm qua OnFocusChangeListener và đặt lại setSelected(false). Trên iOS: xóa hiệu ứng qua layer.removeAnimation(forKey:).
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm