Przycisk ma na sobie „Zapisz zmiany”, a w drzewie dostępności „Submit”. Audyt zgłasza to od 4.14.0
aria-label nadpisuje widoczny napis przycisku i łamie kryterium 2.5.3. Od axe-core 4.14.0 reguła jest włączona domyślnie: jak to zmierzyć i poprawić.
Przycisk w wygenerowanym panelu ma na sobie napis „Zapisz zmiany”. W drzewie dostępności ten sam przycisk nazywa się „Submit”. Osoba sterująca komputerem głosem mówi „kliknij zapisz zmiany” i nic się nie dzieje, bo oprogramowanie dopasowuje komendę do nazwy dostępnej, a nie do tekstu, który widzi na ekranie.
To nie jest literówka w jednym komponencie. To domyślne zachowanie modelu, który dostaje polecenie „dodaj dostępność” i dokłada aria-label wszędzie tam, gdzie widzi kontrolkę – łącznie z kontrolkami, które już mają poprawną nazwę pochodzącą z własnej treści. Kryterium, które to łamie, ma poziom A, a nie AA, i do października większość audytów w ogóle go nie sprawdzała.
Trzy wzorce, w których generator nadpisuje widoczną etykietę
Wszystkie trzy wyglądają w przeglądzie kodu jak poprawa, nie jak defekt, bo w każdym z nich pojawia się atrybut ARIA tam, gdzie wcześniej go nie było.
Angielska nazwa na polskim interfejsie. Model generuje komponent z angielskimi identyfikatorami i angielskim aria-label, a widoczny tekst tłumaczy na polski – często w osobnym kroku, już z plikiem tłumaczeń.
<!-- widać „Zapisz zmiany", nazwa dostępna to „Submit" -->
<button type="submit" aria-label="Submit">{t('form.save')}</button>
Nazwa rozbudowana „dla czytnika ekranu”. Link z napisem „Czytaj dalej” dostaje aria-label="Czytaj dalej o cenniku usług". Intencja jest słuszna – lista linków w czytniku ekranu przestaje być ścianą identycznych pozycji – ale skutkiem jest nazwa, która zawiera widoczny tekst i jeszcze trzy słowa więcej. Tu kryterium jest spełnione (nazwa zawiera widoczny tekst), natomiast już przy wariancie „Cennik usług – czytaj dalej” kolejność słów się rozjeżdża.
Tekst tylko dla czytnika plus aria-label na tym samym elemencie. Klasa sr-only wkłada do przycisku napis, którego nikt nie widzi, a aria-label na elemencie nadrzędnym i tak go przykrywa. Efekt: trzy różne nazwy tej samej kontrolki, w zależności od tego, kto pyta.
Kolejność, w jakiej przeglądarka wylicza nazwę
Nazwa dostępna nie jest sumą tego, co w kodzie napisano. Jest wynikiem algorytmu o ustalonym pierwszeństwie, opisanego w specyfikacji Accessible Name and Description Computation 1.2. Pierwszy krok, który zwróci niepusty tekst, kończy wyliczanie – pozostałe źródła są ignorowane bez ostrzeżenia.
| Kolejność | Źródło nazwy | Co to znaczy w kodzie z generatora |
|---|---|---|
| 1 | aria-labelledby |
Wystarczy jeden poprawny IDREF, żeby cała treść przycisku przestała mieć znaczenie |
| 2 | Wartość zagnieżdżonej kontrolki | Dotyczy pól, których wartość da się zmienić, gdy siedzą wewnątrz etykiety |
| 3 | aria-label |
Najczęstsze miejsce rozjazdu: atrybut wygrywa z widocznym napisem |
| 4 | Mechanizm języka hosta | <label>, alt, <caption> – to, co w HTML robi to samo bez ARIA |
| 5 | Treść elementu | Tekst wewnątrz przycisku czy linku; brany tylko wtedy, gdy rola na to pozwala |
| 6 | title |
Używany wyłącznie wtedy, gdy nic wcześniejszego nie dało wyniku |
Z tej kolejności wynika reguła, którą da się zapamiętać bez zaglądania do specyfikacji: aria-label nie uzupełnia widocznej treści, tylko ją zastępuje. Dokładanie go do kontrolki, która ma widoczny napis, nie dodaje informacji – zabiera ją.
Dlaczego rozjazd uderza w sterowanie głosem, nie w czytnik ekranu
Kryterium 2.5.3 Label in Name (poziom A) wymaga, by dla kontrolek z etykietą tekstową nazwa zawierała tekst prezentowany wizualnie. Dokumentacja W3C stawia cel wprost: widoczna etykieta jest wyzwalaczem dla aktywacji głosowej, a osoby pracujące głosem używają w komendach tego, co widzą.
Dla czytnika ekranu rozjazd jest co najwyżej niezręczny – użytkownik usłyszy „Submit” i poradzi sobie z kontekstu. Dla sterowania głosem to awaria bez obejścia: komenda nie pasuje do nazwy, kontrolka się nie aktywuje, a na ekranie nie ma żadnej wskazówki, jakiego słowa trzeba było użyć. Ta sama logika obowiązuje coraz częściej w drugą stronę, przy agentach i testach end-to-end, które adresują elementy po roli i nazwie, a nie po selektorze CSS.
Dwa wyjątki warto znać, zanim zacznie się poprawiać wszystko podkreślone w raporcie. Po pierwsze, tekst użyty symbolicznie nie jest widoczną etykietą – znak „x” w roli zamknięcia okna albo „B” na przycisku pogrubienia nie wymaga, żeby nazwa brzmiała „x” czy „B”; przeciwnie, nazwa ma opisywać funkcję. Po drugie, kontrolka ikonowa bez żadnego widocznego tekstu nie ma czego zawierać w nazwie i w tym kryterium nie uczestniczy – choć nadal musi mieć nazwę w ogóle, czego pilnuje osobne wymaganie i osobna reguła audytu. Sam brak nazwy przy widocznym tekście opisuje technika F96.
Od axe-core 4.14.0 reguła jest włączona domyślnie
Powód, dla którego ten defekt siedzi w oddanych projektach latami, jest prozaiczny: automat go nie zgłaszał. Reguła label-content-name-mismatch istniała, ale była eksperymentalna i wyłączona, więc nie uruchamiała jej ani domyślna konfiguracja biblioteki, ani rozszerzenie do przeglądarki. Wpis „enable rule by default” trafił do changelogu axe-core przy wersji 4.14.0 z 5 października 2026. Deque opisuje tę zmianę w notce z 22 października 2026 i uprzedza, że liczba zgłoszeń po aktualizacji wzrośnie, co nie znaczy, że strona się pogorszyła – znaczy, że narzędzie raportuje to, czego wcześniej nie raportowało.
Trzy szczegóły działania reguły mają znaczenie przy czytaniu raportu:
- Porównanie słowo po słowie. Deque podaje, że reguła zestawia widoczną etykietę z nazwą dostępną słowo po słowie, zamiast porównywać całe łańcuchy znaków.
- Treść w nawiasach pomijana. Napis „Pobierz raport (PDF, 2,4 MB)” nie generuje zgłoszenia tylko dlatego, że nazwa nie powtarza zawartości nawiasu.
- Część wyników to „do sprawdzenia”, nie „błąd”. Dokumentacja reguł axe-core klasyfikuje tę regułę jako serious z typem zgłoszenia „failure, needs review”; w 4.14.0 dołożono jeszcze oznaczanie ról opartych o wartość tekstową jako niekompletne. Raport z tej reguły wymaga więc decyzji człowieka, a nie tylko zielonego albo czerwonego pola.
Praktyczny wniosek dla przeglądu kodu: jeśli projekt ma przypiętą starszą wersję axe-core, zielony wynik audytu nic o tym kryterium nie mówi. Sprawdzenie zajmuje jedno polecenie – npm ls axe-core – i jest tańsze niż rozmowa z klientem, który dostał raport z nowszej wersji narzędzia niż ta w waszym CI.
aria-label na div i span jest zabroniony, nie tylko bezskuteczny
Drugi wzorzec, który model produkuje seryjnie, to opisywanie atrybutem ARIA elementów, które nie mają roli przyjmującej nazwę. Specyfikacja WAI-ARIA 1.2 mówi o takich rolach, że element nie obsługuje nazwy od autora, i formułuje to jako zakaz: autorzy nie mogą używać aria-label ani aria-labelledby do nazwania takiego elementu. Lista ról z zakazem nazywania liczy jedenaście pozycji: caption, code, deletion, emphasis, generic, insertion, paragraph, presentation, strong, subscript, superscript.
Rola generic to domyślna rola <div> i <span> – czyli dokładnie tych elementów, na które generator najchętniej wiesza opisy. Algorytm wyliczania nazwy przerywa pracę już w pierwszym kroku, gdy rola węzła zabrania nazywania, i zwraca pusty łańcuch. Napisany opis nie trafia nigdzie. Wyłapuje to osobna reguła aria-prohibited-attr, która w axe-core siedzi w zestawie WCAG 2.0 A i AA, więc jest włączona od dawna – jeśli raport jej nie pokazuje, w repozytorium prawdopodobnie nie ma żadnego uruchomienia axe, a nie żadnego problemu.
Poprawka jest jedna z dwóch i nigdy nie jest nią zostawienie atrybutu: albo element dostaje semantyczny odpowiednik (<button>, <section>, <nav>) i wtedy nazwa ma gdzie się zaczepić, albo atrybut wylatuje, a nazwa pochodzi z widocznego tekstu.
Pomiar: nazwa dostępna wobec tekstu widocznego, na wszystkich kontrolkach naraz
Ręczne klikanie po panelu dostępności w DevTools wystarcza do zdiagnozowania jednego przycisku i nie wystarcza do odbioru strony. Dwa sposoby dają listę wszystkich rozjazdów w jednym przebiegu.
Pierwszy to uruchomienie wyłącznie tej reguły z konsoli, na stronie z wstrzykniętym axe-core:
const wynik = await axe.run(document, {
runOnly: ['label-content-name-mismatch', 'aria-prohibited-attr']
});
console.table(
[...wynik.violations, ...wynik.incomplete].flatMap(r =>
r.nodes.map(n => ({
regula: r.id,
typ: wynik.violations.includes(r) ? 'blad' : 'do-sprawdzenia',
selektor: n.target.join(' '),
html: n.html.slice(0, 90)
}))
)
);
Drugi sposób nadaje się na bramkę w CI, bo nie wymaga interpretowania raportu. Migawka drzewa dostępności w Playwrighcie zapisuje dla każdego węzła rolę i nazwę dostępną w formacie YAML – - button "Zapisz zmiany". Taką migawkę generuje locator.ariaSnapshot(), a expect(locator).toMatchAriaSnapshot() porównuje ją z szablonem w repozytorium. Jeśli ktoś dołoży aria-label do przycisku z widocznym napisem, nazwa w migawce przestanie się zgadzać i test się wywróci – bez wiedzy o WCAG po stronie osoby, która czyta wynik.
Migawka ma zaletę, której raport audytu nie ma: jest czytelna dla kogoś, kto dostępnością się nie zajmuje, i trafia do przeglądu kodu jak każdy inny plik. To samo rozumowanie stoi za zapisywaniem reguł odbioru w repozytorium zamiast w głowie. Jeżeli dopiero układasz warsztat freelancera pracującego z modelem, stałe punkty kontroli przed oddaniem są tą jego częścią, która starzeje się najwolniej – zmieniają się narzędzia, nie pytania.
Lista kontrolna przed oddaniem strony
Kolejność ma znaczenie – pierwsze dwa punkty odsiewają większość zgłoszeń, zanim ktokolwiek zacznie czytać raport. Lista jest przydatna tylko wtedy, gdy wykona ją ktoś inny bez dopytywania, więc zasady spisywania procedury obowiązują tu tak samo jak przy każdej innej powtarzalnej czynności w projekcie.
- Sprawdź wersję axe-core w projekcie (
npm ls axe-core). Poniżej 4.14.0 ta reguła nie działała domyślnie, więc poprzednie zielone wyniki jej nie obejmowały. - Wyszukaj w repozytorium wszystkie wystąpienia
aria-labelna elementach, które mają niepustą treść. To jednocześnie lista kandydatów na rozjazd i lista atrybutów, które prawdopodobnie da się usunąć bez straty. - Usuń
aria-labeliaria-labelledbyz<div>i<span>bez jawnej roli – te opisy nie docierają do nikogo. - Dla kontrolek, które potrzebują dodatkowego kontekstu, przenieś nadwyżkę do
aria-describedby, zamiast rozbudowywać nazwę. - Sprawdź warstwę tłumaczeń osobno: nazwa dostępna i widoczny napis muszą pochodzić z tego samego klucza, inaczej rozjeżdżą się przy pierwszej aktualizacji tekstów.
- Zapisz migawkę drzewa dostępności dla formularzy i nawigacji, i wpuść ją do CI jako test.
- Przejdź jedną ścieżkę krytyczną sterowaniem głosowym systemu, wypowiadając wyłącznie to, co widać na ekranie. To jedyny sprawdzian, którego nie zastąpi żadna reguła.
Decyzja, którą trzeba podjąć przed następnym odbiorem, nie dotyczy tego, czy poprawić znalezione rozjazdy. Dotyczy tego, czy dostępność kontrolek zostaje w projekcie jako test, czy jako jednorazowy audyt. Jeżeli jako audyt, kolejna aktualizacja narzędzia znowu wyprodukuje listę, której nikt nie przewidział w wycenie. Jeżeli jako test – migawka wywróci się w dniu, w którym ktoś doda atrybut, a nie w tygodniu, w którym klient przysłał raport z nowszej wersji axe.