Loader dobiera się do dwóch rzeczy: przewidywanego czasu oczekiwania i tego, czy system wie, ile pracy zostało. Poniżej sekundy wystarcza zmiana stanu klikniętego elementu. Od 1 do 10 sekund sprawdza się spinner w pojedynczym module albo skeleton przy ładowaniu całego widoku, a powyżej 10 sekund potrzebny jest pasek postępu z informacją, ile zostało.
Punktem wyjścia jest pierwsza z heurystyk Nielsena, czyli widoczność stanu systemu: interfejs ma w rozsądnym czasie informować, że pracuje nad żądaniem użytkownika. Nielsen Norman Group opisuje badanie z University of Nebraska-Lincoln, w którym osoby widzące ruchomy pasek postępu były gotowe czekać na stronę około trzy razy dłużej niż osoby bez żadnego wskaźnika. Dobry loader potwierdza, że aplikacja się nie zawiesiła, pokazuje, co się dzieje, a przy dłuższych operacjach pozwala ocenić, czy opłaca się czekać.
W skrócie: jaki loader przy jakim czasie oczekiwania
Progi 0,1 s, 1 s i 10 s pochodzą z klasycznego opracowania Jakoba Nielsena o czasach odpowiedzi. Próg około 3 sekund i opóźnienie 300 ms to praktyka projektowa, którą stosujemy w projektach, a nie wynik badania.
| Przewidywany czas | Rekomendowany wskaźnik | Przykładowy mikrotekst |
|---|---|---|
| Poniżej 0,1 s | Brak loadera, wynik pojawia się od razu | Brak |
| 0,1-1 s | Brak osobnego loadera. Zmiana stanu klikniętego elementu, na przykład przycisku. Spinner tylko z opóźnieniem około 300 ms | Brak albo „Zapisywanie…” na przycisku |
| 1-3 s | Spinner w obrębie modułu albo skeleton, gdy ładuje się cały widok | „Wczytywanie danych…” |
| 3-10 s | Skeleton albo spinner z tekstem, który mówi, co się dzieje. Pasek postępu, gdy zakres pracy jest znany | „Przygotowujemy raport. To potrwa kilkanaście sekund.” |
| Powyżej 10 s | Pasek postępu z procentem albo liczbą kroków, szacowany czas, możliwość anulowania | „Importowanie pliku: 3 z 10 arkuszy” |
Główne typy loaderów: spinner, skeleton i pasek postępu
W aplikacjach webowych używa się trzech rodzajów wskaźników ładowania. Różnią się tym, ile mówią użytkownikowi o strukturze treści i o postępie.
Skeleton screen, czyli ekran szkieletowy
Skeleton to statyczne albo animowane wypełniacze, które przypominają układ docelowej strony. Zamiast pustego ekranu aplikacja pokazuje szare prostokąty i linie w miejscach, w których pojawią się tekst, obrazy i tabele. Użytkownik od razu widzi strukturę widoku, więc ładowanie wydaje się krótsze, a po wczytaniu danych nic nie zmienia położenia.
Skeleton stosuje się przy ładowaniu całych widoków: list, kart z danymi, paneli. Musi odzwierciedlać rzeczywisty układ, czyli nagłówki, akapity i grafiki. NN/g w artykule Skeleton Screens 101 odradza wariant, w którym widać tylko ramę strony (nagłówek, stopkę i tło) bez wypełniaczy treści, bo nie daje on poczucia, że strona stopniowo dochodzi do ostatecznej postaci. Autorzy powołują się przy tym na badanie Mejtofta, Långströma i Söderströma z 2018 roku, „The effect of skeleton screens”, opublikowane w materiałach konferencji ECCE. To badanie zespołu z uczelni, a nie NN/g.
Loadery nieokreślone (indeterminate)
Loader nieokreślony sygnalizuje trwający proces, ale nie mówi, ile czasu zostało. Najczęstszy przykład to spinner, czyli obracająca się ikona, albo pasek z zapętloną animacją bez procentów. Taki wskaźnik nie pokazuje struktury ani postępu, tylko stan „praca w toku”.
Stosuje się go wtedy, gdy system nie zna zakresu zadania ani czasu trwania, na przykład przy oczekiwaniu na odpowiedź serwera. W kodzie jest to element z rolą progressbar bez atrybutu wartości. Brak wartości oznacza dla przeglądarki i technologii asystujących postęp nieokreślony.
Loadery określone (determinate)
Loader określony ma znany zakres: wypełnia się od 0 do 100 procent i często towarzyszy mu liczba, na przykład „8 z 10 plików”. Używa się go, gdy system potrafi określić długość procesu albo liczbę kroków: przy pobieraniu pliku o znanym rozmiarze, imporcie o znanej liczbie rekordów, instalacji złożonej z etapów.
W HTML służy do tego element <progress>. Z atrybutem value, na przykład value=”50″ max=”100″, pokazuje postęp określony. Bez atrybutu value przechodzi w tryb nieokreślony. Wskaźnik określony daje użytkownikowi realną informację, dzięki której przy długiej operacji może zdecydować, czy czeka, czy przerywa. Wytyczne Apple zalecają go wszędzie tam, gdzie da się oszacować postęp.
Jak dobrać loader do sytuacji
O wyborze decydują cztery kryteria: czas oczekiwania, znajomość zakresu pracy, miejsce w interfejsie oraz to, czy operacja blokuje dalszą pracę.
Czas oczekiwania
Najważniejszy jest przewidywany czas trwania operacji. Trzy progi Nielsena, 0,1 sekundy, 1 sekunda i 10 sekund, wyznaczają kolejno granicę wrażenia natychmiastowej reakcji, granicę nieprzerwanego toku myśli i granicę utrzymania uwagi na zadaniu.
Poniżej 0,1 s. Wynik wydaje się natychmiastowy. Loader ani komunikat nie są potrzebne, wystarcza pokazanie rezultatu i zwykła reakcja przycisku na kliknięcie.
Od 0,1 do 1 s. Opóźnienie jest zauważalne, ale nie przerywa toku myśli. NN/g zaznacza, że przy czasie poniżej sekundy wskaźnik ładowania niczego nie poprawia, a animacja, która pojawia się i od razu znika, rozprasza. Użytkownik powinien jednak widzieć, że akcja została przyjęta: przycisk zmienia stan albo etykietę na „Zapisywanie…”. Jeśli czas operacji bywa zmienny, spinner pokazuje się z opóźnieniem około 300 ms, czyli tylko wtedy, gdy operacja rzeczywiście trwa dłużej.
Od 1 do 3 s. Powyżej sekundy użytkownik wyraźnie czeka i potrzebuje wskaźnika, żeby nie stracić pewności, że system pracuje. Przy pojedynczym module wystarcza spinner. Gdy ładuje się cały widok, lepszy jest skeleton, bo pokazuje układ treści i wypełnia ekran na te kilka sekund.
Od 3 do 10 s. To już długie czekanie, więc sam ruch nie wystarcza. Jeśli zakres pracy jest znany, pokazuje się postęp liczbowy albo procentowy, nawet przybliżony. Jeśli nie, do spinnera albo skeletonu dochodzi mikrotekst, który mówi, co się dzieje. Po kilku sekundach komunikat może się zmienić, na przykład z „Trwa ładowanie” na „To potrwa jeszcze chwilę”, co potwierdza, że system nadal działa.
Powyżej 10 s. Około 10 sekund to granica utrzymania uwagi. Po jej przekroczeniu użytkownicy chcą zająć się czymś innym, dlatego NN/g zdecydowanie zaleca wskaźnik procentowy: pasek postępu z procentem, liczbą elementów albo kroków, uzupełniony szacowanym czasem. Przy operacjach trwających minuty lepiej podzielić proces na widoczne etapy albo wykonać go w tle i powiadomić o zakończeniu, na przykład e-mailem.
Znajomość zakresu pracy
Drugie kryterium to wiedza systemu o tym, ile pracy ma do wykonania. Gdy zna rozmiar pliku, liczbę rekordów albo liczbę kroków, powinien to pokazać. Pasek postępu jest wtedy bardziej przydatny niż obracająca się ikona bez kontekstu. Gdy zakres nie jest znany, zostaje loader nieokreślony.
Zdarza się, że zakres staje się znany dopiero w trakcie operacji. Wtedy wskaźnik przechodzi z trybu nieokreślonego w określony, ale w tym samym komponencie i w tym samym miejscu. Wytyczne Apple mówią wprost, żeby nie zamieniać wskaźnika kołowego na pasek w trakcie jednej operacji, bo elementy mają inny kształt i rozmiar, a zmiana zaburza układ. Jeśli istnieje szansa, że postęp da się oszacować, bezpieczniej od początku użyć paska w trybie nieokreślonym.
Miejsce w interfejsie
Loader umieszcza się tam, gdzie użytkownik spodziewa się wyniku akcji. Inny wskaźnik pasuje do całego widoku, inny do pojedynczego modułu.
- Cały widok. Przy przeładowaniu strony albo zmianie widoku sprawdza się skeleton. Utrzymuje stabilny układ, bo tekst i grafiki pojawiają się dokładnie tam, gdzie były wypełniacze.
- Pojedynczy moduł. Wykres na dashboardzie, sekcja komentarzy albo lista w panelu dostają własny spinner albo skeleton wewnątrz kontenera. Reszta strony pozostaje interaktywna. NN/g ujmuje to tak: spinner pasuje do pojedynczego modułu, skeleton do ładowania pełnego ekranu.
- Okno modalne i panel boczny. Treść doczytywana z opóźnieniem ładuje się wewnątrz okna. Blokowanie całego ekranu drugim loaderem niczego nie dodaje.
- Akcja wywołana przyciskiem. Przy zapisie formularza przycisk zmienia etykietę na „Zapisywanie…” i pokazuje mały spinner. Użytkownik widzi, która operacja trwa, co ma znaczenie na ekranach z kilkoma równoległymi akcjami. Na ten czas przycisk dostaje atrybut aria-disabled=”true”, żeby nie dało się wysłać formularza drugi raz.
- Operacja w tle. Gdy zadanie nie blokuje pracy, wystarcza dyskretny wskaźnik w nagłówku albo w rogu ekranu. Po zakończeniu aplikacja pokazuje krótkie powiadomienie, bo zniknięcia małej ikony mało kto zauważa.
Stałe miejsca wskaźników ułatwiają orientację: spinner na przycisku albo pod nagłówkiem listy, skeleton w miejscu docelowej treści, pasek postępu pod tytułem strony lub formularza. Wytyczne Apple zalecają trzymanie wskaźników postępu w przewidywalnym, spójnym położeniu.
Operacje blokujące i działające w tle
Jeśli do zakończenia operacji nie da się zrobić nic innego, interfejs powinien to jasno pokazać i zablokować pozostałe akcje. Przy przetwarzaniu płatności formularz zostaje wygaszony, a na środku pojawia się komunikat „Przetwarzanie płatności…” z animacją.
Jeśli operacja może działać asynchronicznie, blokowanie całej aplikacji jest błędem. Użytkownik ma wiedzieć o trwającym procesie i móc robić coś innego, tak jak przy kopiowaniu plików w systemie operacyjnym. Po zakończeniu dostaje informację zwrotną.
Długie operacje powinny dać się anulować, o ile przerwanie jest bezpieczne dla danych. Pasek postępu z przyciskiem „Anuluj” oddaje użytkownikowi decyzję, czy dalej czekać.
Dobre praktyki projektowania loaderów
Poza wyborem typu liczy się wykonanie: tempo animacji, moment pojawienia się wskaźnika, stabilność układu i treść komunikatów.
Animacja płynna i krótka
Loader ma być zauważalny, ale nie męczący. Zbyt wolna animacja sprawia wrażenie ociężałości, zbyt szybka irytuje. NN/g w artykule o czasie trwania animacji podaje dla pojedynczych zmian w interfejsie zakres od 100 do 500 ms i zauważa, że animacje znacznie częściej są za długie niż za krótkie. Dotyczy to pojawiania się i znikania loadera oraz przejścia od skeletonu do treści. Krzywa łagodząca początek i koniec ruchu, na przykład ease-in-out, usuwa wrażenie szarpnięcia.
Opóźnienie przed pokazaniem loadera
Po wywołaniu akcji interfejs czeka od 300 do 500 ms i dopiero wtedy, jeśli operacja nadal trwa, pokazuje wskaźnik. Dzięki temu spinner nie miga przez ułamek sekundy przy szybkich odpowiedziach. Uzupełnieniem jest minimalny czas wyświetlania, na przykład 500 ms: wskaźnik, który już się pojawił, nie znika natychmiast. Obie wartości są praktyką projektową i ustala się je raz dla całego produktu.
Ten sam wskaźnik od początku do końca operacji
Zmiana formy wskaźnika w trakcie operacji myli użytkownika, bo nie kojarzy on drugiego elementu z kontynuacją tego samego procesu. Jeśli wskaźnik ma przejść z trybu nieokreślonego w określony, robi to w jednym komponencie: pasek z zapętloną animacją zaczyna się wypełniać od bieżącego miejsca. Po zakończeniu loader wygasza się krótkim przejściem, a nie znika skokowo.
Stabilny układ podczas ładowania
Pojawienie się loadera nie może przesuwać treści, która po wczytaniu danych przesunie się jeszcze raz. Takie nieoczekiwane przesunięcia mierzy wskaźnik Cumulative Layout Shift z zestawu Core Web Vitals. Za dobry wynik Google uznaje wartość do 0,1 dla 75. percentyla wizyt i zaleca rezerwowanie miejsca od razu, gdy akcja uruchamia dłuższe żądanie sieciowe.
Skeleton rozwiązuje ten problem z definicji, bo od początku zajmuje tyle miejsca, ile zajmie treść. Spinner i pasek postępu potrzebują kontenera o stałej wysokości, w którym później pojawi się zawartość.
Efekt shimmer z umiarem
Shimmer, czyli poświata przesuwająca się po wypełniaczach skeletonu, sygnalizuje aktywność systemu. Zbyt szybki albo zbyt kontrastowy rozprasza i sprawia kłopot osobom wrażliwym na ruch. Wystarcza delikatny gradient o niskim kontraście, a przy włączonym w systemie ograniczeniu ruchu shimmer jest wyłączony.
Mikrotekst, który informuje
Krótki komunikat jest częścią loadera. „Wczytywanie listy zamówień…”, „Zapisywanie zmian…” albo „Przygotowujemy raport, około 20 sekund” mówią, co się dzieje i ile to potrwa. Samo „Loading…” nie mówi nic, a w polskiej wersji produktu jest też błędem lokalizacji.
Ton komunikatu wynika z głosu produktu. W aplikacji dla konsumentów jest miejsce na lżejsze sformułowanie, w systemie finansowym albo medycznym komunikat pozostaje rzeczowy. Komunikat nie przeprasza i nie obiecuje czasu, którego system nie potrafi oszacować.
Loader nie zastępuje wydajności
Wskaźnik łagodzi skutki opóźnienia, ale go nie usuwa. Najpierw skraca się czas ładowania: krytyczne zasoby wczytują się w pierwszej kolejności, dalsze treści są doczytywane leniwie, a pierwsza użyteczna treść pojawia się jak najwcześniej. Sam loader też nie może obciążać strony, dlatego animacje w CSS są lepsze niż ciężkie pliki GIF i rozbudowane skrypty.
Dostępność loaderów: ARIA i ograniczenie ruchu
Loader widoczny tylko na ekranie nie istnieje dla osoby korzystającej z czytnika ekranu. Kryterium 4.1.3 Komunikaty o stanie z WCAG (poziom AA) wymaga, żeby informacja o stanie, w tym o trwającej operacji, była dostępna programowo, bez przenoszenia fokusu. Czy komunikat faktycznie dociera do czytnika ekranu, pomagają sprawdzić narzędzia do testów dostępności.
Element natywny albo rola progressbar. Natywny element <progress> ma wbudowaną semantykę i dokumentacja MDN zaleca go w pierwszej kolejności. Własny loader zbudowany z elementu div i CSS dostaje atrybut role=”progressbar”. W obu przypadkach wymagana jest nazwa dostępna: widoczna etykieta wskazana przez aria-labelledby albo atrybut aria-label=”Ładowanie wyników”.
Wartość postępu. Loader określony podaje aria-valuenow. Atrybuty aria-valuemin i aria-valuemax są potrzebne tylko wtedy, gdy zakres jest inny niż domyślne 0 i 100. Gdy sama liczba nie oddaje postępu, pomaga aria-valuetext, na przykład „Krok 7 z 10: przesyłanie plików”. Loader nieokreślony nie ma atrybutu aria-valuenow. Jego brak informuje technologię asystującą, że postęp nie jest znany.
Początek i koniec ładowania. Komunikat „Ładowanie danych…” umieszczony w elemencie z atrybutem aria-live=”polite” albo role=”status” zostanie odczytany, gdy się pojawi. Po wczytaniu treści ten sam region ogłasza zakończenie. Region musi istnieć na stronie, zanim zmieni się jego zawartość, a zmian nie może być zbyt wiele, bo czytnik odczyta każdą.
Atrybut aria-busy. Kontener, którego zawartość właśnie się aktualizuje, dostaje aria-busy=”true”. Zgodnie z opisem w MDN technologie asystujące wstrzymują wtedy ogłaszanie zmian do czasu, aż wartość wróci do false. Czytnik nie odczytuje częściowo załadowanej listy, tylko gotowy wynik. Jeśli postęp ładowania regionu pokazuje osobny pasek, region wskazuje go atrybutem aria-describedby.
Ograniczenie ruchu. Zapytanie CSS prefers-reduced-motion pozwala wykryć, że użytkownik włączył w systemie ograniczenie animacji. Shimmer zostaje wtedy zastąpiony statycznymi wypełniaczami, a spinner statyczną ikoną z tekstem. Kryterium 2.3.3 Animacja po interakcji ma poziom AAA, więc nie jest obowiązkowe przy zgodności na poziomie AA, ale dokument W3C opisuje, że u osób z zaburzeniami układu przedsionkowego ruch na ekranie wywołuje nudności i migreny.
Kontrast i rozmiar. Kryterium 1.4.11 Kontrast elementów nietekstowych (poziom AA) wymaga kontrastu co najmniej 3:1 względem sąsiednich kolorów dla komponentów interfejsu i grafik potrzebnych do zrozumienia treści. Jasnoszary spinner na białym tle tego warunku nie spełnia. Przycisk w trakcie ładowania zachowuje tło albo obrys, żeby pozostał rozpoznawalny.
Tekst alternatywny dla ikony. Spinner wykonany jako SVG dostaje role=”img” i aria-label=”Ładowanie” albo element <title> w kodzie SVG. Bez tego czytnik ekranu pominie go albo odczyta jako nieopisaną grafikę.
Loader jako komponent design systemu
Progi czasowe, opóźnienie, mikroteksty i atrybuty ARIA opłaca się ustalić raz, w jednym komponencie, zamiast w każdym widoku osobno. Jakie inne elementy trafiają do takiej biblioteki, opisuje wpis o komponentach podstawowego design systemu. Przy tworzeniu design systemu opisujemy loader razem z regułą doboru do czasu oczekiwania i z wymaganiami dostępności. W istniejącym produkcie stany ładowania sprawdzamy w ramach audytu UX/UI, którego pakiety zaczynają się od 4 500 zł netto. Jeśli wspólnej biblioteki jeszcze nie ma, pomocny będzie wpis o tym, kiedy wdrożyć design system.
Najczęstsze pytania
Od ilu sekund pokazywać loader?
Wskaźnik ładowania jest potrzebny przy operacjach dłuższych niż około 1 sekunda. Poniżej tej granicy wystarcza zmiana stanu klikniętego elementu, a spinner, który pojawia się i od razu znika, tylko rozprasza. Przy zmiennym czasie odpowiedzi spinner pokazuje się z opóźnieniem około 300 ms, czyli wyłącznie wtedy, gdy operacja rzeczywiście się przedłuża.
Spinner czy skeleton: co wybrać?
Spinner pasuje do pojedynczego modułu, na przykład wykresu, karty albo przycisku. Skeleton sprawdza się, gdy ładuje się cały widok, bo pokazuje układ treści i rezerwuje dla niej miejsce. Oba wskaźniki nadają się do oczekiwania krótszego niż około 10 sekund. Przy dłuższym potrzebny jest pasek postępu.
Kiedy potrzebny jest pasek postępu z procentami?
Pasek postępu jest potrzebny zawsze, gdy operacja trwa dłużej niż około 10 sekund, oraz przy krótszych, jeśli system zna zakres pracy: rozmiar pliku, liczbę rekordów albo kroków. Pokazuje, ile już wykonano i ile zostało, więc użytkownik może zdecydować, czy czeka, czy przerywa operację.
Jak zapewnić dostępność loadera dla czytników ekranu?
Loader dostaje natywny element progress albo rolę progressbar z nazwą dostępną. Wskaźnik określony podaje aria-valuenow, nieokreślony go nie ma. Ładowany region oznacza się atrybutem aria-busy, a początek i koniec ładowania ogłasza region z role status. Animacje respektują ustawienie prefers-reduced-motion.
Źródła
- NN/g: Progress Indicators Make a Slow System Less Insufferable (K. Sherwin, 2014)
- NN/g: Response Times: The 3 Important Limits (J. Nielsen, 1993)
- NN/g: Skeleton Screens 101 (S. Tankala, 2023)
- NN/g: Executing UX Animations: Duration and Motion Characteristics (P. Laubheimer, 2020)
- Mejtoft, Långström, Söderström: The effect of skeleton screens (ECCE 2018)
- Apple Human Interface Guidelines: Progress indicators
- MDN: ARIA progressbar role
- MDN: aria-busy
- W3C: Understanding SC 4.1.3 Status Messages
- W3C: Understanding SC 2.3.3 Animation from Interactions
- W3C: Understanding SC 1.4.11 Non-text Contrast
- web.dev: Cumulative Layout Shift (CLS)
- MDN: element <progress>
