Почему доступность важна
Доступность (accessibility, a11y) — это практика создания сайтов, которыми могут пользоваться все люди, включая людей с ограниченными возможностями. Это не только этично, но и расширяет аудиторию и улучшает SEO.
Семантическая вёрстка
Правильные HTML теги — основа доступности:
<!-- ❌ Плохо -->
<div class="header">
<div class="nav">
<div class="link" onclick="...">Главная</div>
</div>
</div>
<!-- ✅ Хорошо -->
<header>
<nav aria-label="Главная навигация">
<a href="/">Главная</a>
<a href="/about">О нас</a>
</nav>
</header>
<main>
<article>
<h1>Заголовок страницы</h1>
<section>
<h2>Подзаголовок</h2>
<p>Контент...</p>
</section>
</article>
</main>
<footer>
<nav aria-label="Дополнительная навигация">...</nav>
</footer>
ARIA атрибуты
// Кнопка-иконка
<button aria-label="Закрыть модальное окно">
<XIcon />
</button>
// Модальное окно
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Подтверждение</h2>
<p id="modal-description">Вы уверены?</p>
</div>
// Состояние загрузки
<button aria-busy={isLoading} disabled={isLoading}>
{isLoading ? 'Загрузка...' : 'Отправить'}
</button>
// Уведомления
<div role="alert" aria-live="polite">
Форма успешно отправлена
</div>
// Навигация с текущей страницей
<nav>
<a href="/" aria-current="page">Главная</a>
<a href="/about">О нас</a>
</nav>
// Раскрывающееся меню
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
aria-haspopup="true"
>
Меню
</button>
<ul id="dropdown-menu" role="menu" hidden={!isOpen}>
<li role="menuitem"><a href="#">Пункт 1</a></li>
</ul>
Управление фокусом
// Фокус-ловушка для модального окна
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement>();
useEffect(() => {
if (isOpen) {
previousFocus.current = document.activeElement as HTMLElement;
modalRef.current?.focus();
} else {
previousFocus.current?.focus();
}
}, [isOpen]);
// Ловушка фокуса
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusable?.length) return;
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') {
onClose();
}
};
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={handleKeyDown}
>
{children}
</div>
);
}
// Skip link для клавиатурной навигации
<a href="#main-content" className="skip-link">
Перейти к основному содержимому
</a>
// CSS для skip link
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Контраст и цвета
/* WCAG AA требует контраст 4.5:1 для текста */
/* WCAG AAA требует контраст 7:1 */
/* ❌ Плохо — низкий контраст */
.text-low-contrast {
color: #999;
background: #fff;
}
/* ✅ Хорошо — достаточный контраст */
.text-good-contrast {
color: #595959;
background: #fff;
}
/* Не полагайтесь только на цвет */
/* ❌ Плохо */
.error { color: red; }
/* ✅ Хорошо — цвет + иконка + текст */
.error {
color: #dc2626;
display: flex;
align-items: center;
gap: 0.5rem;
}
.error::before {
content: '⚠️';
}
/* Фокус должен быть видимым */
:focus {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
/* Не убирайте outline без замены */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
Формы
// Связь label и input
<label htmlFor="email">Email</label>
<input id="email" type="email" required />
// Описание ошибок
<div>
<label htmlFor="password">Пароль</label>
<input
id="password"
type="password"
aria-describedby="password-error password-hint"
aria-invalid={!!error}
/>
<p id="password-hint">Минимум 8 символов</p>
{error && <p id="password-error" role="alert">{error}</p>}
</div>
// Группировка полей
<fieldset>
<legend>Способ доставки</legend>
<label>
<input type="radio" name="delivery" value="pickup" />
Самовывоз
</label>
<label>
<input type="radio" name="delivery" value="courier" />
Курьер
</label>
</fieldset>
Изображения
// Информативное изображение
<img src="/product.jpg" alt="Красный рюкзак с двумя карманами" />
// Декоративное изображение
<img src="/decoration.svg" alt="" role="presentation" />
// Сложное изображение (график)
<figure>
<img
src="/chart.png"
alt="График продаж за 2024 год"
aria-describedby="chart-description"
/>
<figcaption id="chart-description">
Продажи выросли на 25% в Q4 по сравнению с Q1
</figcaption>
</figure>
Тестирование доступности
# Инструменты
# - axe DevTools (расширение браузера)
# - Lighthouse (встроен в Chrome)
# - WAVE (wave.webaim.org)
# - Screen readers: NVDA (Windows), VoiceOver (Mac)
# Автоматические тесты
npm install @axe-core/playwright
// Playwright тест
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage should have no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Заключение
Доступность — это не дополнительная функция, а базовое требование. Семантическая вёрстка, ARIA атрибуты, управление фокусом и достаточный контраст делают сайт доступным для всех. Тестируйте с screen readers и автоматическими инструментами.
Доступный сайт — это хороший сайт. Практики a11y улучшают UX для всех пользователей.
