Lumina
Poradnik / Dostępność cyfrowa

WCAG w praktyce: dlaczego aria-label nie naprawi złego interfejsu

Techniczny przewodnik po dostępności stron i aplikacji: semantyczny HTML, ARIA, klawiatura, focus, formularze, kontrast, czytniki ekranowe i testowanie WCAG 2.2.

Mateusz Malanowski17 min czytania

Dostępność nie zaczyna się od aria-label

Gdy audyt dostępności wykrywa problem, najłatwiej dopisać aria-label, dorzucić role="button" i zamknąć zadanie. Raport przestaje świecić na czerwono. Interfejs nadal nie działa.

aria-label może nadać elementowi dostępną nazwę. Nie sprawi jednak, że div zacznie zachowywać się jak przycisk. Nie doda obsługi klawiatury, nie ustawi poprawnego focusu, nie zakomunikuje stanu komponentu i nie naprawi kolejności elementów w DOM-ie.

Dostępność cyfrowa nie jest warstwą nakładaną na gotową aplikację. Wynika z jej konstrukcji: HTML-u, struktury treści, sposobu obsługi interakcji, komunikatów błędów i zachowania komponentów po zmianie stanu.

Jeżeli fundament jest zły, ARIA staje się taśmą klejącą przyklejoną do pękniętego komponentu.

WCAG to standard techniczny, a nie wtyczka

WCAG 2.2 opisuje dostępność za pomocą czterech zasad. Interfejs powinien być:

  • postrzegalny,
  • funkcjonalny,
  • zrozumiały,
  • kompatybilny z przeglądarkami i technologiami asystującymi.

Pod tymi zasadami znajdują się sprawdzalne kryteria na poziomach A, AA i AAA. Nie jest to lista atrybutów do bezmyślnego przeklejenia do JSX-a.

Jedno kryterium może dotyczyć kilku warstw produktu naraz. Dostępna walidacja formularza wymaga poprawnego HTML-u, sensownego tekstu błędu, zarządzania focusem i informacji dostępnej dla czytnika ekranowego. Sam kolor czerwony nie wystarczy. Samo aria-invalid również.

W praktyce większość produktów celuje w poziom AA. To rozsądna baza, ale zgodność z checklistą nie oznacza automatycznie, że aplikacja jest wygodna dla każdej osoby. WCAG wyznacza wymagania. Jakość implementacji nadal zależy od zespołu.

Najpierw HTML, potem ARIA

Przeglądarka zna zachowanie natywnych elementów. Przycisk można aktywować klawiaturą. Link ma rolę linku. Pole formularza można połączyć z etykietą. Nagłówki budują strukturę dokumentu.

Dlatego ten kod:

<button type="button">Dodaj do koszyka</button>

jest lepszym punktem wyjścia niż:

<div role="button" tabindex="0">Dodaj do koszyka</div>

Drugi wariant wymaga ręcznego odtworzenia zachowania przycisku. Trzeba obsłużyć klawiaturę, focus, stan wyłączenia i oczekiwania użytkownika wobec danej roli. Zespół bierze na siebie pracę, którą przeglądarka wykonałaby za darmo.

Jeszcze gorzej wygląda element klikalny bez żadnej semantyki:

<div className={styles.close} onClick={onClose}>
    <CloseIcon />
</div>

Mysz działa. Tab go pomija. Czytnik nie wie, że element jest interaktywny. Ikona nie mówi, co zostanie zamknięte.

Poprawna wersja nie potrzebuje gimnastyki:

<button
    type="button"
    className={styles.close}
    aria-label="Zamknij okno"
    onClick={onClose}
>
    <CloseIcon aria-hidden="true" />
</button>

Tutaj aria-label ma sens. Uzupełnia przycisk, którego treścią jest wyłącznie ikona. Nie udaje semantyki, której wcześniej nie było.

Nazwa, rola i stan komponentu

Technologia asystująca musi dostać trzy podstawowe informacje:

  • czym jest element,
  • jak się nazywa,
  • w jakim jest stanie.

Dla przycisku rozwijającego panel może to wyglądać tak:

<button
    type="button"
    aria-expanded={isOpen}
    aria-controls="filters-panel"
    onClick={toggleFilters}
>
    Filtry
</button>

Rola wynika z elementu button. Nazwa pochodzi z widocznego tekstu „Filtry”. aria-expanded przekazuje stan, a aria-controls wskazuje kontrolowany obszar.

Losowe dodawanie aria-label potrafi tę informację zepsuć. Atrybut może zastąpić widoczną nazwę innym tekstem. Użytkownik widzi „Szukaj”, a czytnik ogłasza „Uruchom wyszukiwanie produktów w katalogu”. Dla części narzędzi sterowanych głosem to realny problem: komenda oparta na widocznej etykiecie przestaje pasować do nazwy dostępnej.

Jeżeli element ma czytelny tekst, zwykle nie potrzebuje aria-label. Gdy nazwa znajduje się w innym elemencie, można użyć aria-labelledby. Opisy i dodatkowe instrukcje lepiej łączyć przez aria-describedby.

ARIA ma przekazywać brakującą informację. Nie powinna produkować drugiej, konkurencyjnej wersji interfejsu.

Klawiatura obnaża źle zbudowany interfejs

Najprostszy test dostępności nie wymaga żadnej wtyczki. Odłóż mysz i przejdź cały scenariusz klawiszem Tab.

Sprawdź:

  • czy da się dotrzeć do każdego elementu interaktywnego,
  • czy kolejność focusu odpowiada kolejności wizualnej,
  • czy przyciski działają Enterem i spacją,
  • czy linki działają Enterem,
  • czy Escape zamyka modal lub menu, jeśli taki wzorzec został zastosowany,
  • czy focus nie wpada w pułapkę,
  • czy po zamknięciu warstwy wraca w sensowne miejsce.

tabindex="0" może włączyć element do naturalnej kolejności focusu. tabindex="-1" pozwala ustawić focus programowo, ale usuwa element z kolejności obsługiwanej Tabem. Dodatnie wartości tabindex prawie zawsze prowadzą do bałaganu. Tworzą ręczną kolejność, która rozpada się po pierwszej większej zmianie layoutu.

W React lub Vue problem często zaczyna się wtedy, gdy kolejność wizualna powstaje przez CSS, a DOM pozostaje ułożony inaczej. Ekran wygląda poprawnie. Klawiatura i czytnik poruszają się po innej stronie.

To nie jest błąd czytnika. To błąd struktury.

Focus musi być widoczny i przewidywalny

Usunięcie outline bez zaprojektowania zamiennika jest jednym z najprostszych sposobów na wycięcie użytkowników klawiatury z aplikacji.

button:focus {
    outline: none;
}

Ten zapis nie „czyści designu”. Ukrywa informację o bieżącej pozycji użytkownika.

Lepszy wariant wykorzystuje :focus-visible:

.button:focus-visible {
    outline: 3px solid #7c5cff;
    outline-offset: 3px;
}

Focus powinien być dobrze widoczny na różnych tłach. Nie może znikać pod sticky headerem, paskiem cookies ani inną warstwą. WCAG 2.2 wprost dodaje kryterium dotyczące focusu zasłoniętego przez pozostałe elementy interfejsu.

Liczy się też jego pozycja po zmianie stanu. Po usunięciu produktu z koszyka focus nie powinien trafiać na body. Po zamknięciu modala powinien wrócić do elementu, który go otworzył. Po błędzie formularza powinien pomóc znaleźć problem, a nie zostawić użytkownika na przycisku „Wyślij” bez żadnej informacji.

Warstwy interfejsu są miejscem, w którym szybkie implementacje zaczynają się mścić.

Poprawny modal wymaga między innymi:

  • rozpoznawalnej roli dialogu,
  • dostępnej nazwy,
  • przeniesienia focusu do otwartego okna,
  • zatrzymania focusu wewnątrz, gdy dialog jest modalny,
  • obsługi zamknięcia,
  • powrotu focusu do elementu otwierającego,
  • niedopuszczenia do interakcji z treścią znajdującą się pod spodem.

role="dialog" nie realizuje żadnego z tych zachowań. Tylko opisuje element.

Podobnie jest z dropdownem. Nie każdy rozwijany panel jest menu w rozumieniu ARIA. Nawigacja, lista opcji formularza, selektor i panel filtrów mają inne wzorce klawiaturowe. Dodanie role="menu", bo komponent wizualnie wygląda jak menu, może narzucić użytkownikowi zestaw zachowań, których kod nie obsługuje.

Najpierw wybiera się właściwy wzorzec interakcji. Dopiero potem role i atrybuty.

Formularz musi wyjaśniać, co poszło nie tak

Placeholder nie jest etykietą. Znika po rozpoczęciu wpisywania, zwykle ma słaby kontrast i nie daje użytkownikowi trwałego kontekstu.

Pole powinno mieć prawdziwy label:

<label for="email">Adres e-mail</label>
<input
    id="email"
    name="email"
    type="email"
    autocomplete="email"
    aria-describedby="email-hint email-error"
    aria-invalid="true"
/>
<p id="email-hint">Na ten adres wyślemy potwierdzenie.</p>
<p id="email-error">Wpisz poprawny adres e-mail.</p>

Komunikat „Nieprawidłowa wartość” jest technicznie widoczny, ale bezużyteczny. Dobry błąd mówi, które pole jest niepoprawne i co trzeba zmienić.

Kolor nie może być jedynym sygnałem. Czerwona ramka bez tekstu nie wyjaśnia problemu osobie, która nie rozróżnia koloru, korzysta z czytnika albo zwyczajnie nie wie, czego oczekuje formularz.

Po nieudanym wysłaniu długiego formularza warto przenieść focus do podsumowania błędów albo pierwszego niepoprawnego pola — zależnie od konstrukcji procesu. Podsumowanie powinno zawierać linki prowadzące do konkretnych miejsc. Użytkownik ma dostać plan naprawy, a nie czerwony ekran.

Kontrast to nie tylko kolor tekstu

Najbardziej znane progi WCAG AA to:

  • co najmniej 4.5:1 dla zwykłego tekstu,
  • co najmniej 3:1 dla dużego tekstu,
  • co najmniej 3:1 dla istotnych elementów interfejsu i stanów graficznych.

Samo przepuszczenie tekstu przez miernik kontrastu nie kończy tematu.

Trzeba sprawdzić również:

  • obramowania pól formularza,
  • ikonę wyszukiwania lub zamknięcia,
  • focus ring,
  • zaznaczony checkbox,
  • aktywny tab,
  • wykresy i legendy,
  • tekst po najechaniu i w stanie disabled,
  • komunikaty błędów,
  • treść umieszczoną na zdjęciu lub gradiencie.

Stan komponentu nie powinien być kodowany wyłącznie kolorem. Aktywny element może dostać ikonę, podkreślenie, tekst lub zmianę kształtu. Kolor może wzmacniać komunikat, ale nie może być jego jedynym nośnikiem.

Problem często siedzi w systemie projektowym. Jeśli token text-muted ma zbyt słaby kontrast, błąd powtarza się na dziesiątkach ekranów. Poprawianie każdego wystąpienia osobno jest stratą czasu. Trzeba naprawić token i zasady jego użycia.

Dynamiczny interfejs musi informować o zmianach

Aplikacje webowe zmieniają treść bez przeładowania strony. Użytkownik widzący ekran zauważy toast, licznik koszyka albo nowy zestaw wyników. Czytnik ekranowy może nie dostać żadnej informacji.

Do komunikowania ważnych zmian służą między innymi regiony live i role statusowe. Trzeba używać ich oszczędnie. Jeśli połowa aplikacji ma aria-live, czytnik zaczyna ogłaszać serię nakładających się komunikatów.

Komunikat powinien być krótki i odpowiadać na pytanie: co właśnie się zmieniło?

Przykłady:

  • „Produkt dodany do koszyka”,
  • „Znaleziono 24 produkty”,
  • „Nie udało się zapisać zmian”,
  • „Kod rabatowy został zastosowany”.

W aplikacji SPA trzeba również pilnować zmian widoku. Po przejściu na nową trasę sam adres URL może się zmienić, ale focus pozostaje w starym miejscu. Użytkownik czytnika nie wie, czy nawigacja się udała. Rozwiązaniem może być świadome zarządzanie focusem oraz komunikowanie tytułu lub głównego nagłówka nowego widoku.

Czytnik ekranowy nie naprawia DOM-u

Czytnik ekranowy nie widzi designu. Odczytuje drzewo dostępności zbudowane na podstawie DOM-u, semantyki i ARIA.

Jeżeli dokument ma sześć nagłówków h1, potem przeskakuje do h4, a sekcje istnieją wyłącznie jako wizualnie duże napisy, nawigacja po strukturze będzie chaotyczna.

Jeśli obraz dekoracyjny ma opis „fioletowa fala gradientowa w tle”, użytkownik musi wysłuchać informacji, która niczego nie wnosi. Dekoracje powinny mieć pusty alt albo zostać ukryte przed drzewem dostępności. Obraz przekazujący informację potrzebuje opisu zależnego od kontekstu, a nie automatycznej nazwy pliku.

Test czytnikiem warto zacząć od kilku pytań:

  • Czy struktura nagłówków ma sens bez CSS?
  • Czy landmarki pozwalają szybko przejść do nawigacji i treści głównej?
  • Czy każdy przycisk ma jednoznaczną nazwę?
  • Czy stan rozwinięcia, zaznaczenia i błędu jest ogłaszany?
  • Czy formularz da się zrozumieć bez patrzenia na ekran?
  • Czy dynamiczne zmiany są komunikowane w odpowiednim momencie?

Nie trzeba znać wszystkich skrótów każdego czytnika. Trzeba umieć przejść kluczowe ścieżki i usłyszeć produkt tak, jak słyszy go użytkownik.

Refaktor komponentów zamiast łatania ekranów

Jeżeli ten sam niedostępny przycisk występuje w trzydziestu miejscach, trzydzieści ticketów nie rozwiązuje problemu. Rozwiązaniem jest jeden poprawny komponent.

Warstwa komponentów powinna wymuszać dobre podstawy:

  • Button renderuje natywny button,
  • Link nie udaje przycisku i odwrotnie,
  • IconButton wymaga dostępnej nazwy,
  • Input łączy label, opis i komunikat błędu,
  • Modal zarządza focusem,
  • Tabs implementują właściwy wzorzec klawiaturowy,
  • komponenty mają przewidywalne stany hover, focus, active, disabled i error.

TypeScript może pilnować części kontraktu. IconButton bez aria-label lub innej dostępnej nazwy powinien być trudny do utworzenia. Pole formularza bez etykiety nie powinno przechodzić code review jako „tymczasowe”.

Dostępność staje się wtedy częścią API komponentu. Nie zależy od pamięci osoby składającej kolejny ekran.

Dostępność w dużym e-commerce

Przy rozwoju interfejsu Media Expert pracowaliśmy nad elementami, których problemy nie kończyły się na jednym widoku. Komponent użyty w wielu ścieżkach zakupowych przenosił ten sam błąd do menu, list produktów, modali albo formularzy.

Praca obejmowała dodawanie brakujących nazw dostępnych, porządkowanie ról, poprawę kontrastu, obsługę czytników ekranowych oraz refaktor komponentów, których struktura utrudniała dostępność.

Najważniejsza lekcja była prosta: przy dużej aplikacji nie opłaca się przykrywać problemów kolejnymi atrybutami. Trzeba ustalić, czy komponent ma właściwy element HTML, nazwę, stan i zachowanie klawiaturowe. Potem poprawić źródło i sprawdzić wszystkie miejsca użycia.

Takie podejście skaluje się lepiej niż seria lokalnych wyjątków. Jest też bezpieczniejsze dla produktu, bo poprawka trafia do wspólnej warstwy zamiast tworzyć kilka wersji tego samego zachowania.

Więcej o pracy przy dużej platformie e-commerce opisujemy w projekcie Media Expert. Dostępność łączymy tam z wydajnością, utrzymaniem komponentów i rozwojem złożonego frontendu. Zobacz też, jak podchodzimy do frontend developmentu.

Automatyczny audyt nie wystarczy

Lighthouse, axe i reguły lintujące są przydatne. Szybko wykrywają brakujące etykiety, część problemów z kontrastem, niewłaściwe role i podstawowe błędy struktury.

Nie sprawdzą jednak, czy kolejność focusu ma sens biznesowy. Nie ocenią, czy tekst błędu rzeczywiście pomaga. Nie przejdą rozsądnie skomplikowanego procesu zakupowego. Nie powiedzą, że modal zwraca focus w absurdalne miejsce albo że komunikaty regionu live są tak częste, że nie da się ich słuchać.

Zielony raport oznacza tylko, że narzędzie nie znalazło problemów, które potrafi znaleźć.

Automatyzację warto włączyć do CI i Storybooka, ale trzeba dołożyć:

  • test klawiaturą,
  • test z czytnikiem ekranowym,
  • powiększenie interfejsu i reflow,
  • kontrolę kontrastu w różnych stanach,
  • sprawdzenie błędów i dynamicznych komunikatów,
  • przejście pełnych procesów, a nie pojedynczych ekranów.

Jak przeprowadzić audyt WCAG

Audyt nie powinien zaczynać się od skanowania całej domeny i eksportu pięciuset identycznych ostrzeżeń.

Najpierw wybiera się reprezentatywne ścieżki:

  1. nawigacja i wyszukiwanie,
  2. logowanie lub rejestracja,
  3. formularz kontaktowy albo checkout,
  4. lista i szczegóły produktu lub usługi,
  5. modal, dropdown, filtry i inne złożone interakcje,
  6. błędy, puste stany i komunikaty po wykonaniu akcji.

Następnie trzeba połączyć kilka metod:

1. Kontrola struktury

Sprawdź landmarki, nagłówki, kolejność DOM-u, etykiety, linki, przyciski i alternatywy tekstowe.

2. Automatyczny skan

Uruchom narzędzia na wybranych widokach. Grupuj błędy według źródła w komponencie, nie tylko według adresu strony.

3. Pełna obsługa klawiaturą

Przejdź każdy scenariusz bez myszy. Obserwuj kolejność i widoczność focusu, pułapki oraz zachowanie warstw.

4. Test czytnikiem ekranowym

Sprawdź nazwy, role, stany, strukturę, formularze i komunikaty dynamiczne.

5. Kontrast, zoom i reflow

Oceń wszystkie stany komponentów. Powiększ tekst i sprawdź, czy treść nie znika ani nie wymaga przewijania w dwóch kierunkach tam, gdzie nie jest to konieczne.

6. Priorytetyzacja

Najpierw naprawiaj blokery kluczowych procesów i błędy wspólnych komponentów. Jeden refaktor Modal albo Input może usunąć problem z wielu ekranów naraz.

7. Regresja

Dodaj testy i zasady code review. Inaczej ten sam błąd wróci przy następnym komponencie.

Checklista dostępnego interfejsu

  • Strona ma poprawny język dokumentu i sensowny tytuł.
  • Nagłówki opisują strukturę, a nie tylko wygląd tekstu.
  • Istnieje możliwość pominięcia powtarzalnej nawigacji.
  • Linki i przyciski używają właściwych elementów HTML.
  • Każdy element interaktywny ma jednoznaczną nazwę.
  • Stany expanded, selected, checked, invalid i disabled są dostępne programowo.
  • Cały kluczowy proces działa bez myszy.
  • Kolejność focusu odpowiada strukturze i zadaniu użytkownika.
  • Focus jest widoczny i nie znika pod innymi warstwami.
  • Modale oraz podobne komponenty poprawnie przejmują i oddają focus.
  • Pola formularzy mają trwałe etykiety.
  • Instrukcje i błędy są połączone z właściwymi polami.
  • Błąd nie jest komunikowany wyłącznie kolorem.
  • Tekst, kontrolki i stany spełniają wymagania kontrastu.
  • Informacyjne obrazy mają sensowne teksty alternatywne.
  • Obrazy dekoracyjne nie zaśmiecają drzewa dostępności.
  • Ważne zmiany dynamiczne są komunikowane bez zalewania użytkownika komunikatami.
  • Interfejs pozostaje użyteczny po powiększeniu tekstu i zmianie szerokości viewportu.
  • Automatyczny test jest uzupełniony testem klawiaturą i czytnikiem.
  • Poprawki trafiają do wspólnych komponentów, jeśli problem występuje systemowo.

Podsumowanie

aria-label jest narzędziem. Małym i konkretnym. Nie jest strategią dostępności.

Dostępny interfejs zaczyna się od poprawnego HTML-u. Potem dochodzą przewidywalna klawiatura, widoczny focus, zrozumiałe formularze, czytelny kontrast, komunikowanie zmian i testy z technologiami asystującymi.

Najdroższe błędy zwykle nie siedzą w pojedynczym atrybucie. Siedzą w źle zaprojektowanym komponencie powielonym w całej aplikacji.

Dlatego dobry audyt WCAG nie kończy się listą ostrzeżeń. Powinien wskazać źródło problemu, priorytet oraz miejsce, w którym poprawka da największy efekt. Czasem będzie to jeden label. Częściej — refaktor komponentu, który od początku udawał coś, czym nie był.

Źródła techniczne

Dostępność cyfrowa

Chcesz poprawić dostępność bez dokładania kolejnych obejść?

Porozmawiajmy o refaktorze frontendu