Ostatnio pracowałem z firmą developerską nad sklepem internetowym. W rekomendacjach z audytu napisałem w punkcie o koszyku, że powinien być zgodny z kryterium 4.1.3 Komunikaty o stanie. Pracowaliśmy na wspólnym arkuszu w Google, więc zaraz obok tej mojej rekomendacji znalazło się pytanie, że przecież koszyk jest cały czas widoczny, więc o co chodzi?
No i właśnie. “Wszyscy widzą, że się dodało”. Tylko że nie wszyscy patrzą na koszyk. Niektórzy słuchają. A żeby było jeszcze śmieszniej - dopisałem mój komentarz w kolejnej kolumnie. Tam wyjaśniłem, o co chodzi i jakie techniki zastosować. Tylko że mój rozmówca... nie widział mojego wyjaśnienia, bo na ekranie się nie zmieściło. Pomyślałem sobie, że to może być ciekawy materiał do Dostępnika i ani słowa w nim o sztucznej inteligencji:)
Kryterium 4.1.3 domaga się, żeby komunikaty o stanie dało się programowo określić — przez atrybut role lub właściwości — tak, aby technologie wspomagające mogły je przedstawić użytkownikowi bez konieczności skupienia na elemencie. No bo przecież fokus jest teraz na przycisku “Dodaj do koszyka”, a nie na koszyku.
Rozłóżmy to na czynniki, bo każde słowo tu jest na wagę złota:
Komunikat o stanie — WCAG definiuje to jako zmianę treści, która niesie informację o wyniku działania (”Dodano do koszyka”), stanie aplikacji (”Oczekiwanie na płatność”), postępie procesu (”Przesyłanie 45%”) albo wystąpieniu błędu (”Kupon nieprawidłowy”).
Programowo określone — musi być oznaczone w kodzie (rolą lub właściwością), a nie tylko narysowane. Sama cyferka obok wózka to nie status; <span role=”status”> z tekstem to już status.
Bez skupienia — czytnik nie ma czekać, aż użytkownik do tego przeskoczy. Ma to powiedzieć od razu, z własnej woli.
Z jednej strony to kryterium mówi technologiom wspomagającym “teraz powiedz coś użytkownikowi”. A z drugiej — co równie ważne — mówi im, czego nie mają mówić: nic, co nie jest komunikatem o stanie. Ale to zostawiam na koniec.
Nie każda zmiana na stronie to komunikat o stanie. I to jest kluczowa precyzja, której brakuje większości audytów. Dwie cechy decydują:
Komunikat niesie informację o wyniku działania, stanie aplikacji, postępie procesu albo wystąpieniu błędu.
Komunikat nie zmienia kontekstu — czyli nie zabiera fokusu.
Zobacz na konkrecie. Wyniki wyszukiwania w sklepie to nie komunikat o stanie — to treść, która zastępuje poprzednią. Ale “Wyświetlono 18 wyników” albo “Brak wyników” to już komunikat. Pasek postępu ładowania archiwum zamówień — komunikat. “Twoje zamówienie zostało złożone” — komunikat.
I ważny wyjątek: jeśli coś zabiera fokus, zmienia kontekst — np. modal z błędem przenosi tam kursor — to nie jest komunikat o stanie i 4.1.3 go nie dotyczy. Takie zdarzenie i tak jest ogłaszane przez technologie wspomagające. Częsty błąd: audytor przyczepia się do każdego błędu w dialogu, a ten jest poza zakresem. Ale strzeż się drugiej strony — jeżeli schowasz błąd w diviku i nie dasz mu fokusu, łapiesz się na 4.1.3.
Zostawmy definicje. Jak to technicznie zrobić? Masz w HTML i ARIA kilka sprawnych narzędzi. Ich wspólnym mianownikiem jest region na żywo (live region) — taką nazwą ARIA określa obszar strony, którego zmianami przeglądarka sama zainteresuje czytnik ekranu lub inną technologię asystującą.
Tu jest sedno całej tej zabawy: to nie jest ani proste “dodaj tag”, ani czarna magia. To dobranie roli do sytuacji, a potem pilnowanie kilku atrybutów, które tę rolę dopieszczają. Pójdę od ról, przez ich właściwości, po zasady łączenia.
Dokładnie trzy role są regionami na żywo i to one wykonują pracę pod 4.1.3. Czwarta — progressbar — sprawa, na którą trzeba uważać, więc znalazła się niżej, osobno.
role=”status” — region na żywo dla komunikatów uprzejmych: wyników działania, stanu aplikacji. “2 produkty w koszyku”, “Zapisano zmiany”, “5 wyników wyszukiwania”. Nie przerywa czytania — komunikuje w ciszy, gdy czytnik skończy wątek. Specyfikacja przypisuje mu niejawnie aria-live=”polite” i aria-atomic=”true” (tak, właśnie true — a nie na odwrót, jak się czasem mylnie podaje w sieci). Praktyka bywa kapryśna: technika ARIA22 wprost zaleca dopisywać aria-atomic=”true” ręcznie, bo nie wszystkie środowiska traktują domyślną wartość tak samo. Dlatego w kodzie poniżej widzisz go jawnie.
role=”alert” — dla komunikatów pilnych, przerywających: błąd, ostrzeżenie, nieoczekiwana blokada. “Hasło nieprawidłowe, zostały Ci dwie próby”, “Produkt wyczerpany”. To wyspecjalizowana postać `status`, przetwarzana jako atomowy region na żywo: niesie niejawnie `aria-live=”assertive”` (przerywa czytanie i mówi od razu) oraz `aria-atomic=”true”`. Ostrożnie z użyciem — o tym za chwilę.
role=”log” — region na żywo, który kolejkuje przyrosty informacji, zamiast zastępować poprzednie. To rola dla kanału czatu, logów, listy zdarzeń. Nowa linia dokleja się, a nie czyta w miejscu starej.
Trzy role, trzy pozycje. Status jest cichy, alert jest głośny, log jest uporządkowany. Wybierasz temperaturę do sytuacji.
Tu wielu autorów popełnia błąd i liczę, że Ty go nie popełnisz. role=”progressbar” to nie jest region na żywo. Owszem, jest blisko spokrewniony — w specyfikacji jako pojęcie powiązane widnieje “status” — ale z definicji nie ogłasza zmian automatycznie. Jedynymi wartościami domyślnymi, które niesie, są aria-valuemin=”0” i aria-valuemax=”100”; nie ma tam żadnego aria-live. Sam progressbar to przede wszystkim widget, który mówi o stanie wartości (aria-valuenow), a nie o zmianie.
Kiedy już chcesz o postępie powiadamiać użytkownika czytnika (a w e-commerce to konieczność przy ładowaniu zamówień, płatności, przesyłaniach), nie zamieniaj paska w rozgadany region na żywo i nie aktualizuj go co każdy procent. Częste zmiany wartości w żywym regionie potrafią zatkać i zablokować syntezator mowy. Do komunikowania postępu pod 4.1.3 właściwym narzędziem jest role=”status” z komunikatem etapowym — czytaj “50 proc.”, “75 proc.”, “gotowe” — a nie migający licznik. Sam progressbar zostaw jako komponent wizualny albo steruj jego ogłaszaniem precyzyjnie skryptem (sam aria-valuenow, okazjonalna aktualizacja), bez ubierania w aria-live.
Same role to nie wszystko. Cztery atrybuty decydują o tym, jak region na żywo zachowuje się:
aria-live — ustawia temperaturę regionu wprost, gdy nie chcesz używać gotowej roli albo chcesz ją nadpisać. Wartość polite (czekaj) lub assertive (przerywaj). Uwaga: role=”status” i `role=”alert”` już niosą swoją wartość aria-live, więc pisanie go ręcznie przy nich to najczęściej redundancja dla starszych czytników.
aria-atomic — mówi, czy czytnik ma czytać cały region, czy tylko zmieniony fragment. Wartość true znaczy “przeczytaj całość”. Kluczowe przy licznikach, gdzie zmienia się tylko cyferka — samo `aria-atomic=”true”` sprawia, że czytnik powie “3 produkty w koszyku”, a nie samo “trzy”.
aria-relevant — mówi, jakie zmiany mają być ogłaszane w regionie. Wartość zapisujesz jako listę: additions (dodany węzeł), removals (usunięty), text (zmiana tekstu) albo pojedyncze all (co do zasady: wszystko). Domyślnie, gdy nie podasz atrybutu, region reaguje na additions text — czyli na dodany tekst i węzły, a usunięcia ignoruje. I dobrze: wartości removals i all należy stosować oszczędnie (sama specyfikacja wręcz to zaleca) — czytnik ogłasza usunięcie tylko wtedy, gdy to istotna zmiana stanu (np. ktoś opuszcza pokój czatu), a przy zwykłych komunikatach o stanie bywa to po prostu szum. Poza tym ogłaszanie usunięć to tylko sugestia dla technologii wspomagających — nie są zobowiązane ich respektować.
aria-busy — gdy region jest w trakcie aktualizacji partiami i czytnik nie ma czytać po kawałku. Ustawiasz aria-busy=”true” przed serią zmian, false po. Czytnik wstrzymuje się, aż skończysz i ogłasza całość we właściwym momencie.
Masz to wszystko, ale jak to poskładać? Trzy reguły, które działają w 95% przypadków:
Rola przed atrybutem. Jeżeli istnieje rola pasująca do sytuacji (status, alert, log) — użyj jej zamiast surowego aria-live. Rola niesie sens i domyślne zachowanie; aria-live to wyjście awaryjne. Ręczne aria-live=”polite” bez roli bywa na dłuższą metę mniej czytelne dla zespołu.
aria-atomic to najczęściej Twój przyjaciel. Przy komunikatach, które w całości niosą sens (licznik, potwierdzenie, błąd) — dodawaj aria-atomic=”true”, nawet przy role=”status”. Owszem, specyfikacja przypisuje status domyślną wartość true, ale technika ARIA22 wprost radzi zapisywać ją jawnie, bo nie wszystkie środowiska traktują domyślną wartość jednakowo. Dlatego w przykładzie niżej widzisz go jawnie — to jest redundandne, ale świadoma decyzja. Z kolei aria-live przy status/alert już by został wpisany wbrew zaleceniu — nie rób tego.
Nie mieszaj temperatór w jednym regionie. Jeden region na żywo = jeden tryb. Pilne i uprzejme komunikaty trzymaj w osobnych regionach, inaczej czytnik nie będzie wiedział, kiedy przerywać, a kiedy czekać.
Dobra wiadomość jest taka, że masz cztery narzędzia. Zła — że samo wklejenie role=”status” nic nie da, jeżeli zrobisz to w złym momencie albo oznaczysz złą część.
Po pierwsze, rola musi wisieć tam od początku, zanim jakiekolwiek treści do niej wpadną. role=”status” na kontenerze dodanym do strony razem z komunikatem nie zadziała — czytnik płynnie porusza się po drzewie i doczyta go dopiero przy kolejnym przejściu, które może nie nastąpić od razu. Kontener ma żyć w DOM-ie przed zmianą; zmienia się tylko jego treść.
Po drugie, nie komunikuj samej cyferki. Nawet gdyby mój wczorajszy koszyk dostał aria-live, komunikat “3” to za mało. Czytnik powie “trzy”. Trzy co? Buty? Pary? Kilometry? Jeżeli cały ciąg “3 produkty w koszyku” nie jest oznaczony jako jedna całość, zwłaszcza gdy zmienia się tylko cyfra, użytkownik usłyszy lakoniczne “trzy”. Understanding — objaśnienie kryterium — ma na to bezpośredni przykład z koszyka, jakby ktoś pisał o twoim sklepie.
Typowy polski koszyk to coś takiego:
<span class=”cart-count”>3</span>
<span>produkty</span>
Cyferka, słówko, koniec. Niewinne z wyglądu, ale nieczytelne dla czytnika: rozbite na dwa elementy, z których zmienia się tylko jeden. Poprawnie to mniej więcej tak:
<span role=”status” aria-atomic=”true”>
<span class=”sr-only”>Koszyk: </span>
<span class=”cart-count”>3 produkty</span>
</span>
Kontener z role=”status” wisi w kodzie od początku. Treść w środku — “3 produkty” plus ukryte “Koszyk: “ dla kontekstu — zmienia się w całości. aria-atomic=”true” wymusza przeczytanie całego ciągu. Czytnik nie ma już szans powiedzieć tylko “trzy”. A aria-live? Nie wpisujesz go tutaj — role=”status” domyślnie niesie polite, jawny zapis byłby tu redundancją.
Dwa przykłady z innych typów komunikatów:
<!-- pilny błąd walidacji -->
<p role=”alert”>Kod rabatowy NIEZAWODKA nie istnieje. Sprawdź literówkę.</p>
<!-- postęp procesu składania zamówienia: etapowy komunikat, bez paska -->
<p role=”status”>Przesyłanie zamówienia: 45%</p>
Trzy role? Nie — w drugim przykładzie celowo znów status. Postęp procesu pod 4.1.3 podajesz jako uprzejmy, etapowy komunikat, a nie jako rozgadany progressbar. Jedyny wspólny mianownik tych trzech sytuacji: czytnik ogłasza je bez proszenia o skupienie.
A teraz to, o czym wspomniałem na początku — i czego nikt nie lubi mówić głośno. 4.1.3 to jedno z tych kryteriów, które można spełnić tak, że użytkownik będzie miał gorzej, niż gdyby strona w ogóle nie była “dostosowana”.
role=”alert” i aria-live=”assertive” to broń, nie flaga świąteczna. Jeżeli oznaczysz alertem każdą drobnostkę — “Dodano do koszyka”, “Zapisano”, “Kliknięto” — czytnik przerywa czytanie bez przerwy. Użytkownik z czytnikiem słucha wtedy serii komunikatów, które nie pozwalają mu dokończyć ani jednego zdania na stronie. To jest gorsze niż cisza: cisza da się ogarnąć, przerwany wątek już nie.
Zasada jest prosta: uprzejmość to domyślny tryb, pilność to wyjątek. “Dodano do koszyka” nie ma prawa przecinać czytanego zdania o właściwościach materiału — to status. “Hasło nieprawidłowe, zostały Ci dwie próby” — to jest usprawiedliwione przerwanie, alert. Jak w przyzwoitej rozmowie: ktoś krzyczy, kiedy się dzieje coś naprawdę ważnego albo groźnego, a nie na każde skinienie.
Druga pułapka to dobry pomysł w złym miejscu. role=”alert” na regionie żyjącym od początku, do którego wpadają treści równolegle z tym, co użytkownik wpisuje — w konfiguratorze, w kreatorze zamówienia, edytorze tekstu — potrafi przerywać użytkownikowi w trakcie pisania. Każde pojawienie się nowej wartości w polu obok wstrzymuje mu oddech w połowie zdania. Tu z pomocą przychodzą ci wiedza z sekcji o aria-busy i aria-relevant: sterujesz kiedy i co ma być ogłoszone.
I trzecia, najgorsza: naddostępność. Niektóre zespoły, chcąc pokazać dobrą wolę, obwieszczają alertem absolutnie wszystkie przejściowe efekty — suwaki, wskaźniki, animacje, ticker kursów. Czytnik zamienia się w teleprompter, który mówi non stop. Taki “dostępny” sklep jest mniej dostępny niż sklep, w którym po prostu zostałoby domyślne ustawienie.
Dobra reguła, którą warto zapisać zespołowi na ścianie: 4.1.3 nie każe Ci wymyślać nowych komunikatów. Nie zmusza, żebyś dodał potwierdzenie tam, gdzie projekt go nie zakłada. Ono tylko każe, żeby te komunikaty, które już są, zostały odczytane — i to w sposób, który użytkownikowi pomaga, nie przeszkadza. Mniej znaczy tu naprawdę więcej.
A gdzie w tym wszystkim zwykła walidacja formularzy? Tu przyda się precyzja, bo ten temat bywa mylony częściej niż którykolwiek inny. Status message z 4.1.3 to nie jest synonim “błędu walidacji”. Kryteria z wytycznej 3.3 — 3.3.1 Identyfikacja błędu i 3.3.3 Sugestia usuwania błędu — to one rządzą tym, jak pola formularza mają być oznaczone i jak użytkownik ma się dowiedzieć, co i gdzie poszło nie tak (oznaczenia błędów, aria-describedby, sugestie poprawnych wartości, fokus w pierwsze pole z błędem). 4.1.3 wnosi co innego: ogłoszenie zmiany, która następuje bez przeniesienia fokusu i bez odświeżenia strony. Dlatego:
Gdy błąd urodzi się w bezpośrednim sąsiedztwie pola — np. użytkownik zgubi literówkę w kodzie rabatowym i natychmiast dostaje czerwoną ramkę z podpisem — to sprawa 3.3.1/3.3.3, zwłaszcza jeśli fokus ląduje w polu z błędem. Nie doklejasz tu od razu role=”alert”.
Gdy komunikat o wyniku dokumentuje się na stronie bez fokusu i bez kontekstu — np. kupon został użyty, liczba pozycji wzrosła, zamówienie przyjęte — to jest status message i tu pracuje 4.1.3. „Invalid entry” w objaśnieniu kryterium to właśnie ten drugi przypadek: ogłoszenie, które przychodzi do czytnika samo.
Nie chodzi więc o to, żeby wpisywać 4.1.3 w rubrykę “walidacja” — tylko żeby każde ogłoszenie zmian zachodzących poza fokusem było podane czytnikowi w odpowiedni sposób. A gdy komunikat łączy oba światy (błąd i nagłą zmianę), mapujesz go zgodnie z tym, co faktycznie się dzieje na ekranie i gdzie ląduje fokus.
Wróćmy do sedna. 4.1.3 dotyczy nie tylko koszyka. To esencja całego e-commerce:
komunikat o dodaniu do koszyka,
licznik produktów,
zmiana stanu “Dostępny / Wyprzedany”,
błąd kodu rabatowego,
przeliczanie wysyłki po zmianie adresu,
postęp płatności,
potwierdzenie złożenia zamówienia,
“produkt niedostępny w tej ilości”.
Jeden programista sam tego nie ogarnie. Ten temat flirtuje z product managementem i designem: jeżeli nie wiesz, które zdarzenia są komunikatami o stanie, a które nie, zrób z zespołem mapowanie wszystkich zdarzeń dynamicznych w aplikacji i przypisz każdemu jedną z kategorii: “status message”, “alert”, “treść/modal” albo “nie komunikować w ogóle”. To spokojnie robota na warsztat przy każdej większej przebudowie sklepu — a przy okazji mocno ogranicza ryzyko nadużyć z poprzedniej sekcji, bo świadomie decydujecie, co i jak ma być ogłaszane.
I właśnie to — ta wspólna decyzja, a nie sam tag — jest prawdziwą różnicą, którą robi 4.1.3. Wtedy rozmówca z firmy developerskiej na pytanie “co to znaczy być zgodnym z tym kryterium” będzie mógł odpowiedzieć bez zawahania: “wiemy, które komunikaty mamy, i wiemy, jak je ogłaszać, żeby pomagały”.
Na moim kanale YouTube zaczynają się pojawiać kolejne materiały. Już wylądowało 19 odcinków podcastu Salonik u Jacka. To były świetne rozmowy o dostępności z ludźmi na bardzo wiele tematów. Pojawił się też pierwszy film z AutomaticA11y 2026, ten który zrobił ogromne wrażenie, jak mogę wnioskować z ewaluacji. To prezentacja Natalii Pacygi na temat wykorzystania AI przez Głuchych. Pojawią się też filmy z poprzednich edycji, a to dzięki Januszowi Olszyńskiemu i Ani Czapnik z Fundacji Kinematograf.
Indie też egzekwują dostępność. SEBI, indyjski regulator rynku kapitałowego, przesunął termin audytów i napraw dostępności na 31 października 2026. Ciekawy przypadek tego samego kierunku, co u nas: kończy się epoka “standardu, który można odkładać”.
Formularze pod lupą. Deque rozkłada na czynniki, jak oznaczać pola wymagane w formularzach dostępnych — od required, przez komunikaty o błędach, po kontekst dla czytników. Szczegółowe jak podręcznik, idealne pod motyw dzisiejszego numeru.
Modal nie zawsze musi zamykać się po kliknięciu w tło. Adrian Roselli odpala solidną dyskusję: klik w zewnętrze okna dialogowego bywa pułapką, zwłaszcza dla klawiatury. A że dialogi zamykające po kliknięciu w tło to specjalność sklepów internetowych — warto przeczytać, zanim następny “wyskakujący” komunikat o wyłączności promocji wyląduje na produkcji.
☕ Dawno się nie przymilałem o kawę, więc przypomnę. Możesz postawić mi wirtualną kawę, którą zamierzam zamienić na tę prawdziwą. Oczywiście tylko wtedy, gdy podoba Ci się Dostępnik i uznajesz moją pracę za wartościową. Z góry też za to dziękuję.
I to by było na tyle. Do listy płonących krajów sprzed tygodnia dokładam Belgię i Chorwację. Być może za chwilę Niemcy, Holandia i kto tam wie, kto jeszcze. Mam nadzieję, że to lato zbliża się do końca. Tymczasem nadal cieszę się AI i Hermesem, który jest coraz mądrzejszy i lepiej mnie zna. Pomaga mi w codziennej pracy, więc pewnie zacznę pisać też wyłącznie o nim, ale to w innym miejscu. W sobotę jedziemy całą rodziną do Zakopanego do teatru Witkacego. Od przyjazdu pociągu do spektaklu mamy prawie 4 godziny czekania. Jeżeli masz pomysł, gdzie możemy odpocząć, to daj proszę znać. Trzymaj się i ciśnij na dostępność, bo dzięki niej świat jest choć trochę bardziej znośny.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.