Zbuduj nakładkę audytu dostępności na żywo, którą wstrzykniesz wszędzie
Lighthouse i axe są doskonałe — i są też przełączeniem kontekstu. Otwierasz DevTools, uruchamiasz audyt, czytasz raport, wracasz do strony. Do szybkiego pierwszego przejścia w trakcie budowania chcesz, aby problemy były narysowane na samej stronie: czerwony kontur wokół każdego obrazu bez tekstu alternatywnego, plakietka nad każdym nieopisanym polem. Ten artykuł buduje dokładnie to jako regułę JavaScript, którą za pomocą JustZix wstrzykniesz na dowolną stronę.
Czym ta nakładka jest — a czym nie jest
To test dymny pierwszego przejścia. Wyłapuje tanie, częste, mechaniczne błędy — te, które odpowiadają za dużą część prawdziwych niepowodzeń dostępności — i pokazuje je wizualnie, abyś nie mógł ich przeoczyć. Nie jest zamiennikiem axe-core czy Lighthouse: nie testuje głęboko semantyki ARIA, kolejności fokusu ani zachowania czytnika ekranu. Traktuj to jak linter, który uruchamiasz nieustannie, a nie pełny audyt, który uruchamiasz przed wydaniem.
Całość to jedna reguła JS. Wstrzyknij ją, rzuć okiem na stronę, napraw to, co się zapali, wstrzyknij ponownie.
Szkielet
Będziemy zbierać problemy do tablicy, rysować kontur plus plakietkę dla każdego i pokazywać licznik podsumowania. Zacznij od rusztowania.
(() => {
// Clean up a previous run
document.querySelectorAll('.jz-a11y').forEach(n => n.remove());
const issues = [];
function flag(el, label, color) {
if (!el || !el.getBoundingClientRect) return;
const r = el.getBoundingClientRect();
if (r.width === 0 && r.height === 0) return; // skip hidden
issues.push({ label });
const box = document.createElement('div');
box.className = 'jz-a11y';
Object.assign(box.style, {
position: 'absolute',
left: (r.left + scrollX) + 'px',
top: (r.top + scrollY) + 'px',
width: r.width + 'px',
height: r.height + 'px',
outline: '2px solid ' + color,
background: color + '22',
zIndex: 2147483000,
pointerEvents: 'none',
boxSizing: 'border-box',
});
const tag = document.createElement('span');
tag.textContent = label;
Object.assign(tag.style, {
position: 'absolute', left: 0, top: '-18px',
font: '11px/1.4 system-ui, sans-serif',
background: color, color: '#fff',
padding: '1px 5px', whiteSpace: 'nowrap',
});
box.appendChild(tag);
document.body.appendChild(box);
}
Jedno wywołanie flag() na element problematyczny. Rejestruje problem i maluje absolutnie pozycjonowane pudełko nad elementem z opisaną plakietką. Wszystko nosi klasę jz-a11y, aby następne uruchomienie mogło to wyczyścić.
Sprawdzenie 1 — obrazy bez alt
<img> w ogóle bez atrybutu alt to niepowodzenie; alt="" jest poprawne (oznacza obraz jako dekoracyjny), więc flagujemy tylko faktycznie brakujący atrybut.
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('alt')) {
flag(img, 'img: no alt', '#e11d48');
}
});
Sprawdzenie 2 — pola formularza bez etykiety
Pole wejściowe potrzebuje dostępnej nazwy. Może ona pochodzić z opakowującego lub powiązanego <label>, z aria-label albo z aria-labelledby. Jeśli żadnej nie ma, pole jest bezużyteczne dla czytnika ekranu.
const fields = 'input:not([type=hidden]):not([type=submit])'
+ ':not([type=button]), select, textarea';
document.querySelectorAll(fields).forEach(el => {
const byFor = el.id &&
document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
const wrapped = el.closest('label');
const aria = el.getAttribute('aria-label')
|| el.getAttribute('aria-labelledby')
|| el.getAttribute('title');
if (!byFor && !wrapped && !aria) {
flag(el, 'input: no label', '#f59e0b');
}
});
Sprawdzenie 3 — puste linki i przyciski
Link lub przycisk bez treści tekstowej i bez dostępnej etykiety jest ogłaszany po prostu jako „link” lub „przycisk”. To wyjątkowo częste przy przyciskach z samą ikoną.
document.querySelectorAll('a, button').forEach(el => {
const text = (el.textContent || '').trim();
const aria = el.getAttribute('aria-label')
|| el.getAttribute('title');
const img = el.querySelector('img[alt]:not([alt=""])');
const hasName = text || aria || img;
if (!hasName) {
const what = el.tagName === 'A' ? 'link' : 'button';
flag(el, 'empty ' + what, '#8b5cf6');
}
});
Sprawdzenie 4 — tekst o niskim kontraście
Pełna matematyka kontrastu WCAG jest złożona, ale sprawdzenie względnej luminancji wyłapuje najgorszych winowajców. Oblicz luminancję dla koloru tekstu i rozstrzygniętego tła, a potem weź współczynnik kontrastu.
function lum(rgb) {
const c = rgb.map(v => {
v /= 255;
return v <= 0.03928
? v / 12.92
: Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126*c[0] + 0.7152*c[1] + 0.0722*c[2];
}
function parse(str) {
const m = str.match(/\d+(\.\d+)?/g);
return m ? m.slice(0, 3).map(Number) : null;
}
function bgOf(el) {
let n = el;
while (n) {
const bg = getComputedStyle(n).backgroundColor;
if (bg && !/rgba?\(0, 0, 0, 0\)|transparent/.test(bg)) {
return parse(bg);
}
n = n.parentElement;
}
return [255, 255, 255];
}
document.querySelectorAll('p, span, a, li, h1, h2, h3, h4, td')
.forEach(el => {
if (!(el.textContent || '').trim()) return;
const fg = parse(getComputedStyle(el).color);
const bg = bgOf(el);
if (!fg || !bg) return;
const l1 = lum(fg) + 0.05, l2 = lum(bg) + 0.05;
const ratio = l1 > l2 ? l1 / l2 : l2 / l1;
if (ratio < 4.5) {
flag(el, 'contrast ' + ratio.toFixed(1) + ':1', '#0ea5e9');
}
});
Próg 4.5:1 to minimum WCAG AA dla tekstu o normalnym rozmiarze. Duży tekst dostaje przepustkę przy 3:1, więc spodziewaj się kilku fałszywych alarmów na dużych nagłówkach — przyjrzyj się im okiem.
Sprawdzenie 5 — skoki kolejności nagłówków i brak lang
Nagłówki nie powinny przeskakiwać poziomów — <h2> bezpośrednio po którym idzie <h4> psuje strukturę dokumentu. A główny <html> potrzebuje atrybutu lang, aby czytniki ekranu wybrały właściwą wymowę.
let prev = 0;
document.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(h => {
const level = +h.tagName[1];
if (prev && level > prev + 1) {
flag(h, 'jump h' + prev + '→h' + level, '#db2777');
}
prev = level;
});
if (!document.documentElement.getAttribute('lang')) {
issues.push({ label: 'html: no lang' });
}
Licznik podsumowania
Na koniec stała plakietka w rogu z sumą. To wynik na pierwszy rzut oka; kontury to szczegóły.
const sum = document.createElement('div');
sum.className = 'jz-a11y';
sum.textContent = issues.length
? issues.length + ' a11y issue(s)'
: 'No quick a11y issues found';
Object.assign(sum.style, {
position: 'fixed', right: '12px', bottom: '12px',
font: '13px/1.5 system-ui, sans-serif',
background: issues.length ? '#e11d48' : '#16a34a',
color: '#fff', padding: '6px 12px', borderRadius: '6px',
zIndex: 2147483600, pointerEvents: 'none',
});
document.body.appendChild(sum);
console.table(issues);
})();
console.table(issues) na końcu zrzuca też uporządkowaną listę do Output Console, więc masz zarówno wizualną nakładkę, jak i podsumowanie do skopiowania.
Jak używać tego na co dzień
- Wrzuć cały skrypt do reguły JS ograniczonej do budowanej strony i uruchamiaj go na żądanie z panelu JS (Ctrl+Enter), aby nie przerysowywał się przy każdej nawigacji.
- Kontury są absolutnie pozycjonowane, więc po dużej zmianie rozmiaru po prostu uruchom go ponownie.
- Naprawiaj najgłośniejsze problemy najpierw — brakujący alt i nieopisane pola to zwykle jednoliniowe poprawki w HTML.
- Gdy plakietka w rogu zrobi się zielona, przełącz się na Lighthouse lub axe do głębokiego audytu. Ta nakładka uprząta łatwe 80%; prawdziwe narzędzia zajmą się resztą.
Zobacz też
- Nakładka do debugowania responsywności — ten sam pomysł zastosowany do błędów układu.
- Wyłącz dark patterns i fałszywą pilność — naprawianie innego rodzaju zepsutego UX.
Nakładka audytu na żywo zmienia dostępność z listy kontrolnej, o której zapominasz, w coś, czego nie da się zignorować. Zainstaluj JustZix, wklej skrypt i pozwól stronie powiedzieć Ci, co jest nie tak.
Oceń ten wpis
Brak ocen — oceń jako pierwszy.