← Назад к вопросам
Как реализуется доступность сайта в верстке?
2.0 Middle🔥 171 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализуется доступность (Accessibility) в верстке
Доступность - это процесс сделать сайт удобным для всех, включая людей с инвалидностью. Расскажу о конкретных практиках.
1. Семантический HTML
Фундамент доступности - использование правильных HTML элементов:
<!-- ❌ Неправильно - div везде -->
<div onclick="navigate('/about')">About Us</div>
<!-- ✅ Правильно - семантические элементы -->
<nav>
<a href="/about">About Us</a>
</nav>
<!-- ✅ Правильная структура -->
<header>
<nav>...</nav>
</header>
<main>
<article>
<h1>Заголовок статьи</h1>
<section>
<h2>Раздел 1</h2>
<p>Содержимое</p>
</section>
</article>
</main>
<footer>
Подвал сайта
</footer>
Почему это важно:
- Скринридеры понимают структуру
- Клавиатурная навигация работает
- SEO лучше
- Код понятнее
2. ARIA атрибуты
когда HTML недостаточно, используем ARIA:
<!-- Для пользователей скринридера -->
<button aria-label="Закрыть меню">X</button>
<!-- Скринридер скажет "Закрыть меню" вместо просто "X" -->
<!-- aria-describedby -->
<input
id="password"
type="password"
aria-describedby="pwd-hint"
/>
<p id="pwd-hint">Пароль должен быть минимум 8 символов</p>
<!-- aria-expanded для меню -->
<button
aria-expanded="false"
aria-controls="menu"
onclick="toggleMenu()"
>
Меню
</button>
<div id="menu" hidden>
<!-- Меню -->
</div>
<!-- aria-live для динамического контента -->
<div aria-live="polite" role="status">
<!-- Сообщения об ошибках, которые появляются -->
<!-- Скринридер автоматически прочитает новый текст -->
</div>
<!-- role для кастомных компонентов -->
<div role="button" tabindex="0" onclick="handleClick()">
Кастомная кнопка
</div>
3. Клавиатурная навигация
<!-- Все интерактивные элементы должны быть доступны с клавиатуры -->
<!-- tabindex для контролирования порядка табуляции -->
<input tabindex="1" placeholder="Первое поле" />
<input tabindex="2" placeholder="Второе поле" />
<button tabindex="3">Отправить</button>
<!-- ❌ Плохо -->
<div onclick="doSomething()">Нажми</div>
<!-- Это невозможно активировать с Enter/Space -->
<!-- ✅ Хорошо -->
<button onclick="doSomething()">Нажми</button>
<!-- Работает с клавиатурой автоматически -->
Обработка клавиатуры в React:
export function CustomButton() {
const handleClick = () => {
console.log("Clicked");
};
const handleKeyDown = (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
};
return (
<div
role="button"
tabindex="0"
onClick={handleClick}
onKeyDown={handleKeyDown}
>
Кастомная кнопка
</div>
);
}
4. Alt текст для изображений
<!-- ✅ Хороший alt текст - описывает что находится на картинке -->
<img src="cat.jpg" alt="Рыжий кот спит на диване" />
<!-- ❌ Плохой alt текст -->
<img src="cat.jpg" alt="cat" />
<img src="cat.jpg" alt="image123" />
<!-- Для декоративных изображений - пустой alt -->
<img src="divider.png" alt="" />
<!-- aria-hidden также помогает скрыть от скринридера -->
<img src="icon.svg" aria-hidden="true" />
5. Контраст цветов
/* WCAG требует минимум 4.5:1 контраст для текста */
/* ❌ Плохой контраст -->
.text {
color: #999999; /* светло-серый */
background: #eeeeee; /* очень светло-серый */
/* Контраст: 1.59:1 - недостаточно */
}
/* ✅ Хороший контраст -->
.text {
color: #333333; /* тёмно-серый */
background: #ffffff; /* белый */
/* Контраст: 12.6:1 - отлично */
}
Проверить контраст: https://webaim.org/resources/contrastchecker/
6. Размер текста и шрифт
/* Текст должен быть читаем -->
.text {
font-size: 16px; /* Минимум 14-16px для основного текста */
line-height: 1.5; /* Достаточное расстояние между строками */
letter-spacing: 0.5px; /* Может помочь людям с дислексией */
font-family: sans-serif; /* Без засечек проще читать */
}
/* Пользователи могут увеличивать размер -->
/* Не используй: font-size: 12px; */
/* Используй: font-size: 0.75rem; чтобы работало масштабирование */
7. Форма и валидация
<!-- Правильная структура формы -->
<form>
<label for="email">Email:</label>
<input
id="email"
type="email"
required
aria-describedby="email-error"
/>
<span id="email-error" role="alert">
<!-- Сообщения об ошибках -->
</span>
</form>
React компонент:
export function AccessibleForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const handleChange = (e) => {
setEmail(e.target.value);
setError("");
};
const handleBlur = () => {
if (!email.includes("@")) {
setError("Пожалуйста, введите корректный email");
}
};
return (
<form>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={handleChange}
onBlur={handleBlur}
aria-describedby={error ? "email-error" : undefined}
/>
{error && (
<span id="email-error" role="alert" style={{ color: "red" }}>
{error}
</span>
)}
</form>
);
}
8. Цвет не единственный способ передачи информации
<!-- ❌ Плохо - информация только в цвете -->
<div style="color: red">Обязательное поле</div>
<!-- ✅ Хорошо - используем цвет + символ -->
<div style="color: red">* Обязательное поле</div>
<!-- Для графиков - используем разные паттерны -->
<canvas id="chart"></canvas>
<!-- + предоставляем таблицу с данными -->
<table>...</table>
9. Фокус видимости
/* Не убирай outline без замены! -->
/* ❌ Плохо */
button {
outline: none; /* Плохо - теряется видимость фокуса */
}
/* ✅ Хорошо */
button {
outline: 2px solid #4299e1;
outline-offset: 2px;
}
/* Или кастомный стиль */
button:focus-visible {
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
10. Моторные нарушения
<!-- Большие кликабельные области -->
<button style="padding: 12px 24px; min-width: 44px; min-height: 44px;">
Удалить
</button>
<!-- WCAG рекомендует минимум 44x44 px -->
<!-- Не требуй точность -->
<!-- ❌ Плохо -->
<button style="width: 20px; height: 20px;">X</button>
<!-- ✅ Хорошо -->
<button style="width: 40px; height: 40px; padding: 8px;">
<span aria-hidden="true">X</span>
<span className="sr-only">Закрыть</span>
</button>
11. Skip-link для клавиатурной навигации
<!-- Первый элемент в body -->
<a href="#main-content" className="skip-link">
Перейти к основному контенту
</a>
<nav>
<!-- Много ссылок в навигации -->
</nav>
<main id="main-content">
<!-- Основной контент -->
</main>
/* Skip-link скрывается, но видна при фокусе -->
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0; /* Становится видна при фокусе */
}
12. Testing доступности
# Инструменты
axe DevTools - Chrome extension
Lighthouse - в Chrome DevTools
Wave - Firefox/Chrome
ScreenReader - NVDA (Windows), JAWS
Автоматизированные тесты:
import { render, screen } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
test("Button is accessible", async () => {
const { container } = render(
<button>Click me</button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
13. Чеклист доступности
const a11yChecklist = {
semantic_html: "Используются <button>, <a>, <input> вместо div",
alt_text: "Все изображения имеют alt текст",
keyboard_navigation: "Все функции доступны с клавиатуры",
aria_labels: "Используются aria-label где нужны",
color_contrast: "Контраст > 4.5:1",
focus_visible: "Видна граница фокуса",
form_labels: "Поля связаны с label",
error_messages: "Ошибки описаны clearly",
mobile_friendly: "Работает на мобильных",
skip_links: "Есть возможность пропустить много ссылок",
no_autoplay: "Нет автозапуска видео/звука",
testing_done: "Протестировано со скринридером"
};
Главное правило
Доступность - это не feature, а requirement. Это значит что не нужно добавлять её в конце - нужно строить с самого начала.
Простой способ помнить: если бы ты пользовался сайтом:
- Только с клавиатурой (без мышки)
- С закрытыми глазами (скринридер)
- На мобильном телефоне (маленький экран)
Все ли работало бы? Если да - сайт доступен.