Веб-доступность на практике: Роли ARIA и руководство по соответствию WCAG 2.2
前端工程
Четыре принципа WCAG 2.2
WCAG 2.2 организует требования соответствия вокруг принципов POUR:
| Принцип | Значение | Основное требование | Пример |
|---|---|---|---|
| Воспринимаемость | Информация представима | Контент воспринимаем всеми пользователями | Alt изображений, субтитры, контраст |
| Управляемость | Интерфейс управляем | Используем через несколько методов ввода | Навигация с клавиатуры, достаточно времени, без мигания |
| Понятность | Контент понятен | Содержание и работа ясны | Согласованная навигация, подсказки ошибок, помощь при вводе |
| Надёжность | Контент совместим | Разбираем вспомогательными технологиями | Валидный HTML, семантика ARIA |
Система ролей ARIA
Роли структуры документа
<header role="banner">
<nav role="navigation" aria-label="Main navigation">
<ul role="menubar">
<li role="menuitem"><a href="/home">Home</a></li>
<li role="menuitem" aria-haspopup="true">
Products
<ul role="menu">
<li role="menuitem"><a href="/saas">SaaS</a></li>
<li role="menuitem"><a href="/api">API</a></li>
</ul>
</li>
</ul>
</nav>
</header>
<main role="main">
<article role="article">
<h2>Article Title</h2>
<p>Content body</p>
</article>
</main>
<footer role="contentinfo">Copyright info</footer>
Роли виджетов
| Роль | Назначение | Обязательные свойства | Ключевое взаимодействие |
|---|---|---|---|
button |
Кликабельный триггер | Нет | Enter/Пробел активирует |
checkbox |
Отмечено/не отмечено | aria-checked |
Пробел переключает |
radio |
Одиночный выбор | aria-checked |
Стрелки переключают |
slider |
Значение диапазона | aria-valuenow, aria-valuemin, aria-valuemax |
Стрелки регулируют |
tablist / tab |
Панели вкладок | aria-selected |
Стрелки переключают |
dialog |
Модальное/немодальное | aria-modal |
Esc закрывает |
combobox |
Выборочный ввод | aria-expanded |
Стрелка выбрать/фильтр ввода |
tree / treeitem |
Древовидная структура | aria-expanded |
Стрелка развернуть/свернуть |
Свойства и состояния ARIA
Краткий справочник основных свойств
<!-- Labels and descriptions -->
<button aria-label="Close dialog">✕</button>
<input aria-labelledby="name-label" aria-describedby="name-hint">
<span id="name-label">Username</span>
<span id="name-hint">6-20 characters</span>
<!-- States -->
<div aria-expanded="true" aria-selected="false" aria-checked="mixed">
<div aria-hidden="true" aria-disabled="true" aria-busy="true">
<!-- Relationships -->
<div aria-owns="child-id" aria-controls="panel-id" aria-flowto="next-section">
<!-- Live regions -->
<div role="alert" aria-live="assertive">Error message</div>
<div aria-live="polite" aria-atomic="true" aria-relevant="additions text">Status update</div>
Первое правило: Не злоупотребляйте ARIA
Если нативный HTML-элемент уже обеспечивает нужную семантику и взаимодействие, не пере implementируйте с помощью ARIA.
<!-- ❌ Anti-pattern -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<!-- ✅ Correct -->
<button type="submit">Submit</button>
Практика: Доступное модальное окно
<div class="modal-overlay" id="modalOverlay" hidden>
<div
role="dialog"
aria-modal="true"
aria-labelledby="modalTitle"
aria-describedby="modalDesc"
class="modal"
>
<h2 id="modalTitle">Confirm Deletion</h2>
<p id="modalDesc">This action cannot be undone. Are you sure you want to delete this record?</p>
<div class="modal-actions">
<button class="btn-cancel">Cancel</button>
<button class="btn-confirm">Confirm Delete</button>
</div>
</div>
</div>
class AccessibleModal {
constructor(dialogEl) {
this.dialog = dialogEl;
this.previousFocus = null;
}
open() {
this.previousFocus = document.activeElement;
this.dialog.hidden = false;
const firstFocusable = this.dialog.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
document.addEventListener('keydown', this.handleKeydown);
}
close() {
this.dialog.hidden = true;
this.previousFocus?.focus();
document.removeEventListener('keydown', this.handleKeydown);
}
handleKeydown = (e) => {
if (e.key === 'Escape') this.close();
if (e.key === 'Tab') this.trapFocus(e);
};
trapFocus(e) {
const focusable = [...this.dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
Практика: Доступные вкладки
<div class="tabs">
<div role="tablist" aria-label="Account settings">
<button
role="tab"
id="tab-profile"
aria-selected="true"
aria-controls="panel-profile"
tabindex="0"
>Profile</button>
<button
role="tab"
id="tab-security"
aria-selected="false"
aria-controls="panel-security"
tabindex="-1"
>Security</button>
<button
role="tab"
id="tab-notify"
aria-selected="false"
aria-controls="panel-notify"
tabindex="-1"
>Notifications</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
<p>Profile settings content</p>
</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
<p>Security settings content</p>
</div>
<div role="tabpanel" id="panel-notify" aria-labelledby="tab-notify" hidden>
<p>Notification preferences content</p>
</div>
</div>
document.querySelector('[role="tablist"]').addEventListener('keydown', (e) => {
const tabs = [...e.currentTarget.querySelectorAll('[role="tab"]')];
const idx = tabs.indexOf(document.activeElement);
let nextIdx;
if (e.key === 'ArrowRight') nextIdx = (idx + 1) % tabs.length;
else if (e.key === 'ArrowLeft') nextIdx = (idx - 1 + tabs.length) % tabs.length;
else if (e.key === 'Home') nextIdx = 0;
else if (e.key === 'End') nextIdx = tabs.length - 1;
else return;
e.preventDefault();
tabs.forEach((t) => { t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; });
tabs[nextIdx].setAttribute('aria-selected', 'true');
tabs[nextIdx].tabIndex = 0;
tabs[nextIdx].focus();
});
Практика: Доступная форма
<form aria-label="User registration">
<div class="field">
<label for="email">Email address <span aria-hidden="true">*</span></label>
<input
id="email"
type="email"
required
aria-required="true"
autocomplete="email"
aria-invalid="false"
aria-describedby="email-error"
>
<span id="email-error" role="alert" aria-live="assertive"></span>
</div>
<div class="field">
<label for="password">Password</label>
<input
id="password"
type="password"
minlength="8"
aria-describedby="pwd-rules pwd-strength"
>
<span id="pwd-rules">At least 8 characters, including uppercase, lowercase, and digits</span>
<span id="pwd-strength" aria-live="polite"></span>
</div>
<fieldset>
<legend>Notification method</legend>
<label><input type="checkbox" name="notify" value="email"> Email</label>
<label><input type="checkbox" name="notify" value="sms"> SMS</label>
</fieldset>
<button type="submit">Register</button>
</form>
Сравнение инструментов автоматизированного тестирования
| Инструмент | Тип | Покрытие WCAG | Интеграция | Особенность |
|---|---|---|---|---|
| axe-core | Библиотека времени выполнения | A/AA | CI + Браузер | Наиболее полная, нулевые ложные срабатывания |
| Lighthouse | Инструмент аудита | A | CLI / DevTools | Комбинация производительности + a11y |
| WAVE | Расширение браузера | A/AA | Вручную | Визуальные аннотации |
| NVDA | Программа чтения с экрана | — | Вручную | Реальный пользовательский опыт |
| Playwright a11y | E2E-тест | A | CI | Интегрировано в поток тестирования |
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('accessibility check', async ({ page }) => {
await page.goto('/register');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
Новые критерии WCAG 2.2
| Новый критерий | Уровень | Описание |
|---|---|---|
| 2.4.11 Внешний вид фокуса | AAA | Площадь индикатора фокуса ≥ 2px, контраст ≥ 3:1 |
| 2.4.12 Фокус не скрыт | AAA | Элемент в фокусе не скрыт другим содержимым |
| 3.2.6 Согласованная справка | A | Расположение механизма помощи согласовано на всех страницах |
| 3.3.7 Избыточный ввод | A | Не требовать повторного ввода ранее предоставленной информации |
| 3.3.8 Доступная аутентификация | AA | Аутентификация не основана на тестах когнитивных функций |
Резюме лучших практик
- Нативные элементы прежде всего: Используйте
<button>,<input>и т.д. вместо реализации через ARIA - Управление фокусом: Проактивно устанавливайте фокус на логическую позицию после модальных/маршрутных изменений
- Живые регионы с осторожностью: Используйте
aria-live="assertive"только для критических уведомлений - Автоматизированное + ручное: axe-core покрывает ~30-40% проблем; тестирование с программой чтения с экрана необходимо
- Непрерывная интеграция: Включите проверки доступности в конвейер CI для предотвращения регрессий
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →
#无障碍#ARIA#WCAG#屏幕阅读器#可访问性