Przejdź do treści
Dostępność WCAG a kod z modelu

axe nie zgłasza błędów w tym formularzu. Klawiatura i czytnik ekranu znajdują pięć

Automaty pokrywają 16 z 50 kryteriów WCAG AA. Pięć barier w generowanym formularzu z kodem przed i po oraz test klawiaturą, który zajmuje pięć minut.

Redakcjadeveloper front-end9 minkod

Formularz kontaktowy wygenerowany przez model przechodzi audyt automatyczny bez jednego zgłoszenia. Kontrast zgodny, atrybuty obecne, struktura poprawna. Potem ten sam formularz zostaje przejechany klawiszem Tab i czytnikiem ekranu – i wychodzi pięć barier, z których żadnej automat nie miał jak zobaczyć.

Skala tego problemu jest policzona. Raport WebAIM z lutego 2026 podaje, że brakujące etykiety pól formularza występują na 51 proc. stron głównych, a 33,1 proc. wszystkich pól nie ma prawidłowej etykiety, przy średniej 6,9 pola na stronę. Generatory nie poprawiły tej statystyki – przy średniej liczbie pól rosnącej o 36 proc. w trzy lata udział pól bez etykiety utrzymuje się na tym samym poziomie.

Ile automat w ogóle widzi

Zanim przejdziemy do kodu, warto znać rząd wielkości, w którym pracuje narzędzie w potoku budowania. Brytyjska jednostka cyfrowa przepuściła trzynaście walidatorów przez jedną stronę ze 142 znanymi barierami: najlepsze narzędzie wyłapało 40 proc., WAVE 30 proc., axe 29 proc., a wszystkie trzynaście pominęło kolejność czytania, brak widocznego wskaźnika fokusu i kontekstowe użycie koloru.

Drugi punkt odniesienia dotyczy kryteriów, nie liczby błędów: testy automatyczne pokrywają 16 z 50 kryteriów sukcesu WCAG 2.1 na poziomie AA, przy czym po liczbie wykrytych problemów wynik wygląda lepiej, bo zdominowany jest przez kontrast – rzecz, którą automat sprawdza doskonale.

Wniosek dla procesu: zielony wynik w potoku oznacza „nie ma błędów z tych 16 kryteriów”. Formularz żyje w pozostałych 34.

Bariera 1: placeholder w roli etykiety

Najczęstszy wzorzec w kodzie z generatora, bo wygląda czysto i oszczędza miejsce:

<input type="email" placeholder="Twój e-mail" class="input" />

Placeholder znika w momencie, w którym użytkownik zacznie pisać. Osoba, która wróci do pola po minucie, nie ma już informacji, co w nim było – a osoba korzystająca z czytnika ekranu w części konfiguracji nie usłyszy go wcale. Wersja, która działa:

<label for="email">Adres e-mail</label>
<input type="email" id="email" name="email" autocomplete="email" />

Jeśli etykieta nie może być widoczna z powodów projektowych, zostaje ona w kodzie i jest ukrywana wizualnie klasą typu .visually-hidden – nigdy przez display:none, bo to usuwa ją też z drzewa dostępności. Atrybut aria-label jest rozwiązaniem trzeciego wyboru: działa dla czytnika, ale nie daje klikalnego obszaru i nie pomaga osobie, która potrzebuje tekstu na ekranie.

Bariera 2: komunikat błędu, o którym pole nie wie

Generator umieszcza komunikat w elemencie obok pola i na tym kończy. Wizualnie poprawnie, w drzewie dostępności nie istnieje żadne powiązanie:

<input type="email" id="email" />
<span class="error">Podaj poprawny adres</span>

Osoba nawigująca klawiaturą wchodzi w pole i słyszy nazwę pola. Komunikat leży w elemencie, który nie jest częścią opisu pola, więc nie zostanie przeczytany. Poprawka to dwa atrybuty:

<input type="email" id="email" aria-invalid="true"
       aria-describedby="email-error" />
<span class="error" id="email-error">Podaj poprawny adres</span>

aria-describedby dołącza treść komunikatu do opisu pola, a aria-invalid informuje o stanie błędu. Oba muszą być ustawiane i zdejmowane dynamicznie – aria-invalid="true" pozostawione po poprawieniu wartości jest gorsze niż jego brak, bo czytnik raportuje błąd w polu, które jest już poprawne.

Bariera 3: wysłanie formularza, po którym nic nie zostaje powiedziane

Użytkownik klika „Wyślij”, walidacja odrzuca formularz, na górze pojawia się ramka z listą błędów. Na ekranie widać to natychmiast, dla czytnika ekranu nie stało się nic: fokus został na przycisku, a nowy element pojawił się poza jego uwagą.

Dwa mechanizmy, które to zamykają. Pierwszy – kontener komunikatów oznaczony jako obszar aktywny, obecny w kodzie od początku, nie dodawany dopiero przy błędzie:

<div role="alert" aria-live="assertive" id="form-status"></div>

Drugi – przeniesienie fokusu na pierwsze pole z błędem, nie na kontener z listą. Użytkownik ma wtedy kursor w miejscu, które wymaga poprawy, a nie przy opisie problemu:

const first = form.querySelector('[aria-invalid="true"]');
if (first) first.focus();

Ta sama zasada obowiązuje po udanym wysłaniu. Komunikat „dziękujemy” wyrenderowany bez obszaru aktywnego i bez zmiany fokusu oznacza, że osoba korzystająca z czytnika nie wie, czy formularz wyszedł. Zmiana zarządzania fokusem po akcji jest tym samym problemem, który wychodzi w generowanych okienkach modalnych, i w obu przypadkach automat go nie zgłosi.

Bariera 4: wymagane pole oznaczone wyłącznie gwiazdką

Gwiazdka przy etykiecie i legenda „pola oznaczone * są wymagane” na dole formularza to wzorzec, który model powtarza, bo widział go tysiące razy. Problemy są dwa: informacja o znaczeniu gwiazdki jest fizycznie oddalona od pola, a sama gwiazdka bywa czytana jako „gwiazdka” albo pomijana.

<label for="tel">Telefon <span aria-hidden="true">*</span>
  <span class="visually-hidden">(wymagane)</span>
</label>
<input type="tel" id="tel" required />

Atrybut required daje walidację przeglądarki i jest raportowany przez czytnik, a tekst w wersji ukrytej wizualnie usuwa zależność od symbolu. Przy polach, których nie da się oznaczyć atrybutem, bo walidacja jest po stronie skryptu, zostaje aria-required="true".

Bariera 5: grupa pól bez wspólnej nazwy

Zestaw przycisków radiowych albo pól wyboru wygenerowany jako płaska lista traci informację o tym, czego dotyczy wybór:

<p>Preferowany kontakt</p>
<input type="radio" id="k1" name="kontakt" /> <label for="k1">Telefon</label>
<input type="radio" id="k2" name="kontakt" /> <label for="k2">E-mail</label>

Czytnik przeczyta „Telefon, przycisk opcji, 1 z 2″ – bez pytania, na które to odpowiedź. Akapit nad grupą jest tylko tekstem i nie wiąże się z polami. Konstrukcja, która to naprawia, jest w HTML od zawsze:

<fieldset>
  <legend>Preferowany kontakt</legend>
  <input type="radio" id="k1" name="kontakt" /> <label for="k1">Telefon</label>
  <input type="radio" id="k2" name="kontakt" /> <label for="k2">E-mail</label>
</fieldset>

Walidacja z generatora odrzuca poprawne dane

Osobna kategoria, formalnie nie bariera dostępności, a w skutkach ta sama rzecz: użytkownik nie może wysłać formularza. Model generuje wyrażenie regularne do sprawdzania pól, bo tak wygląda większość kodu, na którym się uczył, i wyrażenie to zwykle odrzuca dane poprawne.

// typowy wynik generatora
const telOk = /^\d{9}$/.test(value);
const mailOk = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-z]{2,3}$/.test(value);

Pierwsze wyrażenie odrzuca numer z prefiksem kierunkowym i numer zapisany ze spacjami, czyli dwa najczęstsze sposoby wpisywania telefonu. Drugie odrzuca adresy z kropką lub znakiem plus przed małpą, adresy w domenach dłuższych niż trzy znaki i wszystko, co zawiera znak spoza zakresu ASCII. Osoba z takim adresem nie ma żadnej drogi dalej – komunikat mówi jej, że jej własny adres jest niepoprawny.

Rozwiązanie jest krótsze niż wyrażenie regularne: oddaj walidację przeglądarce, a format znormalizuj po stronie serwera.

<input type="email" id="email" autocomplete="email" required />
<input type="tel" id="tel" autocomplete="tel" inputmode="tel" required />

Typ email ma wbudowaną walidację zgodną ze standardem, inputmode="tel" podnosi właściwą klawiaturę na telefonie, a autocomplete pozwala przeglądarce wypełnić pole danymi, które użytkownik ma już zapisane – co przy nawigacji klawiaturą albo przy drżeniu rąk jest różnicą między formularzem do wypełnienia a formularzem do porzucenia. Numer telefonu sprawdzaj po usunięciu spacji i myślników, nie przed.

Test, który zajmuje pięć minut przed oddaniem

Kolejność ma znaczenie, bo pierwsze trzy kroki wykonuje się bez żadnego narzędzia poza klawiaturą:

  • Odłóż mysz i przejdź formularz klawiszem Tab. Sprawdź, czy widać, gdzie jesteś – brak widocznego wskaźnika fokusu to bariera, której nie zgłosił żaden z trzynastu walidatorów w cytowanym teście.
  • Wyślij formularz z pustymi polami. Czy fokus trafił na pierwsze błędne pole? Czy da się dojść do komunikatu bez myszy?
  • Wejdź w pole, zacznij pisać i wyjdź. Czy nadal wiesz, co w tym polu ma być? Jeśli informacja zniknęła, to był placeholder w roli etykiety.
  • Włącz czytnik ekranu (VoiceOver na macOS, NVDA na Windows) i przejdź formularz raz. Pięć minut, a wyłapuje wszystkie bariery z tego tekstu.
  • Na koniec uruchom automat – po to, żeby złapać kontrast i brakujące atrybuty, czyli dokładnie to, w czym jest dobry.

Kolejność odwrotna, czyli automat na początku, prowadzi do tego, że zielony wynik kończy sprawdzanie – i to jest główny mechanizm, przez który formularze z pierwszego akapitu trafiają do klienta.

Co zapisać w procesie agencji, żeby nie wracało

Pięć barier opisanych wyżej to zawsze te same pięć. Nie trzeba ich pamiętać – trzeba mieć je w jednym miejscu, przez które przechodzi każdy formularz przed oddaniem. Dwie formy, które się sprawdzają: fragment szablonu formularza z poprawnymi atrybutami, używany jako punkt startowy zamiast promptu od zera, oraz pięciopunktowa lista w zadaniu przed przekazaniem projektu.

Formularz wygenerowany od zera w każdym projekcie będzie miał te same braki za każdym razem, bo model nie pamięta poprzedniego zlecenia. Własny szablon z fieldset, aria-describedby i obsługą fokusu po walidacji zamyka temat raz – i jest tą częścią warsztatu, którą opisuje się jako proces przed automatyzacją, a nie po. Przy większej liczbie projektów opłaca się wpiąć ten test w potok budowania razem z pozostałymi kontrolami jakości, co jest zadaniem dla kogoś, kto buduje takie potoki pod konkretny proces w zespole.

Decyzja: jeden formularz w tym tygodniu

Weź najnowszy formularz, który oddaliście klientowi, i przejedź po nim pięciominutowym testem z sekcji wyżej. Nie po to, żeby go naprawić – po to, żeby policzyć, ile z pięciu barier w nim jest. Wynik zwykle mieści się między trzema a pięcioma i to jest liczba, która uzasadnia wprowadzenie szablonu lepiej niż jakikolwiek argument o zgodności.

Jeśli wyjdzie zero, macie proces, który działa, i warto sprawdzić, czy jest zapisany, czy istnieje w głowie jednej osoby. Jeśli wyjdzie trzy albo więcej, decyzją na ten tydzień nie jest audyt dostępności całego serwisu, a jedna rzecz: szablon formularza w repozytorium zespołu, z którego startuje każdy kolejny projekt.