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.
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.