Pasek przewijania (marquee) — to efekt wizualny, w którym tekst automatycznie przewija się poziomo w ograniczonym obszarze, umożliwiając wyświetlanie długich linii w kompaktowym kontenerze. Historycznie termin pochodzi od HTML-znacznika <marquee>, popularnego w sieci w latach 90. XX wieku. We współczesnym developmentcie mobilnym scrolling text jest używany w odtwarzaczach, tickerach informacyjnych i paskach stanu. Według Apple Human Interface Guidelines i Google Material Design, pasek przewijania jest dopuszczalny tylko w ograniczonych scenariuszach, gdzie statyczne przycięcie z wielokropkiem pogarsza UX.
Najważniejsze
Pasek przewijania — to efekt UI, w którym tekst przesuwa się poziomo wewnątrz stałego obszaru, cyklicznie przewijając się, aby użytkownik mógł przeczytać całą treść nie mieszczącą się w kontenerze. W przeciwieństwie do ellipsize, który ukrywa część tekstu, marquee pokazuje cały tekst, ale w ruchu.
Termin marquee (ang. — „namiot”, „billboard reklamowy”) utrwalił się dzięki HTML-znacznikowi <marquee>, który został dodany we wczesnych wersjach przeglądarek Netscape i Internet Explorer. Znacznik pozwalał tworzyć przewijający się tekst bez JavaScript i CSS, ale nigdy nie był częścią standardu HTML. Nowoczesne przeglądarki nadal go obsługują dla zachowania wstecznej kompatybilności, ale jego użycie nie jest zalecane.
W developmentcie mobilnym pasek przewijania rozwiązuje problem długich podpisów w kompaktowych kontenerach: nazwy utworów w odtwarzaczu, tickery kursów walut, paski informacyjne na ekranach telewizyjnych. Jednak ze względu na animację obciąża accessibility i może irytować użytkowników — dlatego stosuje się go punktowo.
Według badań Nielsen Norman Group (2023), ruchomy tekst zmniejsza szybkość czytania o 15-20% i zwiększa obciążenie poznawcze. Używaj paska przewijania tylko wtedy, gdy statyczne przycięcie (ellipsize) jest rzeczywiście nie do zaakceptowania.
Na Androidzie pasek przewijania implementuje się za pomocą wbudowanego mechanizmu TextView poprzez atrybut android:ellipsize="marquee". Jest to jedyny tryb ellipsize, który nie obcina tekstu, a animuje jego przewijanie.
Do aktywacji marquee muszą być spełnione trzy warunki: singleLine="true" (lub maxLines="1"), ellipsize="marquee" i setSelected(true) w kodzie. Bez wywołania setSelected() animacja nie zostanie uruchomiona.
<!-- 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: aktywuj marquee
textView.setSelected(true)
textView.isSingleLine = true
Atrybut marqueeRepeatLimit określa liczbę powtórzeń animacji: marquee_forever (nieskończenie) lub konkretną liczbę. Domyślnie — marquee_forever. Jeśli tekst jest krótszy niż szerokość kontenera, animacja nie zostaje uruchomiona — TextView wyświetla statyczny tekst.
Ważna cecha: setSelected(true) działa tylko gdy TextView ma focus lub znajduje się w trybie selected. W RecyclerView może to powodować problemy — animacja uruchamia się tylko dla jednego elementu. Rozwiązanie — użyj niestandardowego MarqueeTextView, który nadpisuje isFocused() i zawsze zwraca true.
Począwszy od Android 9 (API 28), Google poprawiła zachowanie marquee — animacja stała się płynniejsza i nie zawiesza się podczas scrollowania listy. Jednak na starszych urządzeniach marquee może szarpać, zwłaszcza przy wysokiej gęstości pikseli.
W iOS nie ma wbudowanego odpowiednika android:ellipsize="marquee". Pasek przewijania implementuje się ręcznie przez Core Animation lub za pomocą animacji SwiftUI. Najczęstsze podejście — animacja layer.position w połączeniu z kontenerem clipping.
W UIKit pasek przewijania opiera się na trzech elementach: kontener UILabel z clipsToBounds = true, sam tekst i CABasicAnimation do przesuwania tekstu po X z nieskończonym powtórzeniem i autoreverses = false, aby pasek przewijał się w jedną stronę i wracał skokowo.
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")
}
Obliczenie duration = textWidth / 60 daje prędkość około 60 pikseli na sekundę — komfortowe tempo do czytania. Jeśli prędkość przekracza 120 px/s, tekst staje się nieczytelny. Jeśli poniżej 30 px/s — animacja wygląda ospale. Zmieniaj speed w zależności od konkretnego scenariusza.
W SwiftUI pasek przewijania implementuje się prościej — przez modyfikator offset() z powtarzalną animacją i .animation(.linear(duration:).repeatForever(autoreverses: false)). SwiftUI automatycznie zarządza cyklem życia animacji, ale wymaga GeometryReader do określenia szerokości tekstu.
W sieci przestarzały znacznik <marquee> nie jest zalecany do użycia, choć przeglądarki nadal go obsługują. Nowoczesna implementacja paska przewijania — CSS-animacja z klatkami kluczowymi i transform: translateX.
Zalety podejścia CSS: pełna kontrola nad prędkością, pauzą i krzywą animacji, obsługa prefers-reduced-motion dla accessibility, brak semantycznie niepoprawnego znacznika.
.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;
}
Kluczowa sztuczka: display: inline-block u kontentu gwarantuje, że szerokość elementu jest równa szerokości tekstu, a translateX(-100%) przesuwa go dokładnie o tę szerokość w lewo. Jeśli content ma display: block, translateX(-100%) będzie równy szerokości kontenera, a nie tekstu, i animacja się zepsuje.
Dla accessibility dodaj @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } } — użytkownicy z zaburzeniami przedsionkowymi zobaczą statyczny tekst z możliwością scrollowania.
Pasek przewijania jest uzasadniony w scenariuszach, gdzie użytkownik nie może samodzielnie rozszerzyć kontenera, a pełny tekst jest krytyczny: bieżący utwór w odtwarzaczu na zablokowanym ekranie, pasek informacyjny na panelu telewizyjnym, pasek przewijania w aplikacji taksówkowej (status zamówienia).
Paska przewijania nie należy używać w tabelach, listach, formularzach i dialogach — wszędzie tam, gdzie użytkownik aktywnie współdziała z interfejsem. Animacja rozprasza, wydłuża czas szukania elementu i powoduje dyskomfort. Według WebAIM (2024), 1 na 3 użytkowników z zaburzeniami przedsionkowymi zgłasza dyskomfort podczas oglądania animowanego tekstu.
Zasada kciuka: jeśli tekst można skrócić lub umieścić w następnym wierszu — zrób to. Pasek przewijania to ostateczność, a nie uniwersalne rozwiązanie dla długich podpisów.
Pasek przewijania to jedna z najbardziej problemowych animacji UI z punktu widzenia accessibility. Ruchomy tekst może powodować zawroty głowy, nudności i dezorientację u osób z zaburzeniami przedsionkowymi. Zgodnie z WCAG 2.2, każda animacja trwająca dłużej niż 5 sekund musi mieć mechanizm pauzy lub wyłączenia.
Na iOS użyj właściwości UIAccessibility.isReduceMotionEnabled do sprawdzenia ustawienia systemowego. Jeśli reduceMotion jest włączona — nie uruchamiaj animacji, pokaż statyczny tekst z ellipsize.
Na Androidzie sprawdzaj animację systemową przez Settings.Global.ANIMATOR_DURATION_SCALE. Jeśli globalna animacja jest wyłączona (duration scale = 0), pasek przewijania nie powinien być włączany.
W sieci użyj funkcji medialnej prefers-reduced-motion: @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } }. To standardowa możliwość CSS, obsługiwana przez wszystkie nowoczesne przeglądarki. Sprawdź w DevTools: włącz emulację prefers-reduced-motion i upewnij się, że animacja się wyłącza.
Często zadawane pytania
Ellipsize obcina tekst i wstawia wielokropek, ukrywając część treści. Marquee przewija cały tekst, ale w ruchu. Wybór zależy od priorytetu: pokazać wszystko (marquee) czy zachować statykę (ellipsize).
Wbudowany android:ellipsize="marquee" nie pozwala zmieniać prędkości — jest ona określana przez systemowy ScrollSpeed. Do niestandardowej prędkości użyj Animation lub ValueAnimator ręcznie.
Z powodu ponownego wykorzystania widoków setSelected(true) jest resetowane. Rozwiązanie: utwórz niestandardowy MarqueeTextView, który nadpisuje isFocused() i zwraca true, lub użyj niestandardowej animacji w Adapter.onViewAttachedToWindow().
Tak, przez translateY zamiast translateX. Pionowy marquee jest używany do napisów końcowych (credits) lub pasków informacyjnych w pionowych pasmach. Zasada jest ta sama, ale zmienia się oś animacji.
W CSS: :hover { animation-play-state: paused; }. Na Androidzie: śledź focus przez OnFocusChangeListener i resetuj setSelected(false). Na iOS: usuń animację przez layer.removeAnimation(forKey:).
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również