Собери живой оверлей аудита доступности, который можно внедрить куда угодно
Lighthouse и axe превосходны — и они же являются переключением контекста. Ты открываешь DevTools, запускаешь аудит, читаешь отчёт, прыгаешь обратно на страницу. Для быстрого первого прохода во время сборки тебе нужно, чтобы проблемы были нарисованы на самой странице: красный контур вокруг каждой картинки без alt-текста, бейдж над каждым полем без метки. Эта статья собирает ровно это в виде JavaScript-правила, которое можно внедрить на любой сайт с JustZix.
Чем этот оверлей является — и чем не является
Это дымовой тест первого прохода. Он ловит дешёвые, частые, механические ошибки — те, на которые приходится большая доля реальных провалов доступности, — и показывает их визуально, чтобы ты не смог их пропустить. Это не замена axe-core или Lighthouse: он не тестирует глубоко семантику ARIA, порядок фокуса или поведение скринридера. Считай его линтером, который ты гоняешь постоянно, а не полным аудитом, который запускаешь перед релизом.
Всё это — одно JS-правило. Внедри его, окинь страницу взглядом, почини то, что подсветилось, внедри заново.
Скелет
Мы будем собирать проблемы в массив, рисовать контур плюс бейдж для каждой и показывать сводный счётчик. Начни с каркаса.
(() => {
// 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);
}
Один вызов flag() на каждый проблемный элемент. Он записывает проблему и рисует абсолютно позиционированную коробку поверх элемента с подписанным бейджем. Всё несёт класс jz-a11y, чтобы следующий запуск мог это стереть.
Проверка 1 — картинки без alt
<img> вообще без атрибута alt — это провал; alt="" валидно (оно помечает картинку как декоративную), так что мы помечаем только по-настоящему отсутствующий атрибут.
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('alt')) {
flag(img, 'img: no alt', '#e11d48');
}
});
Проверка 2 — поля формы без метки
Полю нужно доступное имя. Оно может прийти из обёртывающего или связанного <label>, из aria-label или aria-labelledby. Если ничего нет, поле непригодно для использования со скринридером.
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');
}
});
Проверка 3 — пустые ссылки и кнопки
Ссылка или кнопка без текстового содержимого и без доступной метки озвучивается просто как «ссылка» или «кнопка». Это крайне частая беда у кнопок только с иконкой.
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');
}
});
Проверка 4 — текст с низким контрастом
Полная математика контраста WCAG громоздка, но проверка относительной яркости ловит худших нарушителей. Вычисли яркость для цвета текста и для разрешённого фона, затем возьми коэффициент контраста.
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');
}
});
Порог 4.5:1 — это минимум WCAG AA для текста обычного размера. Крупному тексту даётся послабление на 3:1, так что жди несколько ложных срабатываний на больших заголовках — их оцени глазом.
Проверка 5 — пропуски в порядке заголовков и отсутствующий lang
Заголовки не должны пропускать уровни — <h2>, за которым сразу идёт <h4>, ломает структуру документа. А корневому <html> нужен lang, чтобы скринридеры выбрали правильное произношение.
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' });
}
Сводный счётчик
Наконец, фиксированный бейдж в углу с общим числом. Это оценка с одного взгляда; контуры — это детали.
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) в конце также выводит аккуратный список в Output Console, так что у тебя есть и визуальный оверлей, и копируемая сводка.
Как пользоваться этим изо дня в день
- Помести весь скрипт в JS-правило, ограниченное сайтом, который ты собираешь, и запускай его по запросу из панели JS (Ctrl+Enter), чтобы он не перерисовывался при каждой навигации.
- Контуры позиционированы абсолютно, так что после большого изменения размера просто запусти его заново.
- Чини сначала самые громкие проблемы — отсутствующий alt и поля без меток обычно являются однострочными правками HTML.
- Когда угловой бейдж становится зелёным, переключайся на Lighthouse или axe для глубокого аудита. Этот оверлей закрывает лёгкие 80%; настоящие инструменты разбираются с остальным.
Смотри также
- Оверлей для отладки адаптива — та же идея, применённая к багам вёрстки.
- Отключаем тёмные паттерны и фальшивую срочность — починка другого вида сломанного UX.
Живой оверлей аудита превращает доступность из чек-листа, который ты забываешь, в нечто, что нельзя игнорировать. Установи JustZix, вставь скрипт — и пусть страница сама скажет тебе, что не так.
Оцени эту статью
Оценок пока нет — оцени первым.