← Назад к вопросам
Как реализуется доступность HTLM?
2.0 Middle🔥 181 комментариев
#Soft Skills и рабочие процессы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализуется доступность HTML?
Семантический HTML — основа доступности
Доступность (accessibility, a11y) — это обеспечение использования веб-сайта всеми людьми, включая людей с ограниченными возможностями. Начинается с использования правильных HTML элементов вместо универсальных div и span.
Семантические элементы
<!-- === НЕПРАВИЛЬНО: div везде === -->
<div class="header">
<div class="nav">
<div class="nav-item"><a href="/">Home</a></div>
<div class="nav-item"><a href="/about">About</a></div>
</div>
</div>
<div class="main">
<div class="article">
<div class="article-title">Title</div>
<div class="article-content">Content</div>
</div>
</div>
<!-- === ПРАВИЛЬНО: семантические теги === -->
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<article>
<h1>Title</h1> <!-- h1, не div -->
<p>Content</p> <!-- p, не div -->
</article>
</main>
<!-- === Основные семантические элементы === -->
<header>Верхняя часть страницы (логотип, навигация)</header>
<nav>Навигационные ссылки</nav>
<main>Главный контент страницы</main>
<article>Отдельная статья или блог-пост</article>
<section>Тематический раздел контента</section>
<aside>Боковая панель, цитаты, реклама</aside>
<footer>Нижняя часть страницы</footer>
<h1>Главный заголовок (один на странице)</h1>
<h2>Подзаголовок второго уровня</h2>
<h3>Подзаголовок третьего уровня</h3>
<ul><li>Неупорядоченный список</li></ul>
<ol><li>Упорядоченный список</li></ol>
<button>Кнопка (не <div>!)</button>
<label for="email">Email</label>
<input type="email" id="email" />
ARIA атрибуты (Accessible Rich Internet Applications)
// === React компонент с ARIA ===
export function AccessibleDialog() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
{/* Кнопка с описанием для скрин-ридеров */}
<button
onClick={() => setIsOpen(true)}
aria-label="Open settings dialog"
aria-expanded={isOpen}
aria-controls="settings-dialog"
>
Settings
</button>
{/* Диалоговое окно с ARIA ролями */}
{isOpen && (
<div
id="settings-dialog"
role="dialog"
aria-labelledby="dialog-title"
aria-modal="true"
>
<h2 id="dialog-title">Settings</h2>
{/* Закрывающая кнопка */}
<button onClick={() => setIsOpen(false)}>
Close
</button>
{/* Опции с описанием */}
<label htmlFor="theme-select">Theme:</label>
<select id="theme-select" aria-describedby="theme-help">
<option>Light</option>
<option>Dark</option>
</select>
<p id="theme-help">Choose your preferred color scheme</p>
</div>
)}
</>
);
}
// === Частые ARIA атрибуты ===
// aria-label — для элементов без видимого текста
<button aria-label="Close menu">X</button>
// aria-labelledby — связь с текстовым элементом
<h2 id="title">Dialog Title</h2>
<div role="dialog" aria-labelledby="title">Content</div>
// aria-describedby — дополнительное описание
<input aria-describedby="password-hint" />
<p id="password-hint">At least 8 characters</p>
// aria-live — динамические обновления
<div aria-live="polite">
{message} {/* Скрин-ридер объявит новое значение */}
</div>
// aria-hidden — скрыть элемент от скрин-ридеров
<span aria-hidden="true">*</span> {/* визуальный элемент, не важно для a11y */}
// aria-expanded — состояние расширения
<button aria-expanded={isOpen}>Menu</button>
// aria-current — текущая страница в навигации
<a href="/about" aria-current="page">About</a>
Форм и Input
// === ПРАВИЛЬНО: связь label с input ===
export function LoginForm() {
return (
<form>
{/* htmlFor связывает label с input */}
<label htmlFor="email">Email</label>
<input id="email" type="email" required />
<label htmlFor="password">Password</label>
<input id="password" type="password" required />
{/* Явные требования для скрин-ридеров */}
<button type="submit" aria-label="Submit login form">
Sign In
</button>
</form>
);
}
// === Валидация и ошибки ===
export function FormWithValidation() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
if (value && !value.includes('@')) {
setError('Invalid email format');
} else {
setError('');
}
};
return (
<>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={handleChange}
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<p id="email-error" role="alert">
{error}
</p>
)}
</>
);
}
Навигация и структура
// === Правильная структура навигации ===
export function Navigation() {
return (
<nav aria-label="Main navigation">
<ul>
<li>
<a href="/" aria-current="page">Home</a>
</li>
<li>
<a href="/products">Products</a>
</li>
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</ul>
</nav>
);
}
// === Breadcrumb навигация ===
export function Breadcrumb() {
return (
<nav aria-label="Breadcrumb">
<ol>
<li>
<a href="/">Home</a>
<span aria-hidden="true">/</span>
</li>
<li>
<a href="/products">Products</a>
<span aria-hidden="true">/</span>
</li>
<li aria-current="page">
Product Details
</li>
</ol>
</nav>
);
}
// === Пропуск навигации (skip to main) ===
export function SkipLink() {
return (
<a href="#main-content" className="sr-only">
Skip to main content
</a>
// sr-only — видимо только для скрин-ридеров
);
}
Изображения и альтернативный текст
// === Изображения с alt текстом ===
// ПРАВИЛЬНО: описательный alt текст
<img
src="user-avatar.jpg"
alt="Avatar of John Doe, 25 years old user" />
// НЕПРАВИЛЬНО: пустой alt
<img src="user-avatar.jpg" alt="" />
// НЕПРАВИЛЬНО: просто имя файла
<img src="user-avatar.jpg" alt="user-avatar" />
// === Декоративные изображения ===
// Если изображение чисто декоративное:
<img src="decorative-line.png" alt="" aria-hidden="true" />
// === Иконки и символы ===
// ПРАВИЛЬНО: иконка с aria-label
<button aria-label="Close">
<CloseIcon />
</button>
// НЕПРАВИЛЬНО: видимый текст "X" без описания
<button>X</button>
// === Сложные изображения (диаграммы, графики) ===
// ПРАВИЛЬНО: долгое описание
<figure>
<img
src="sales-chart.png"
alt="Sales by quarter 2023"
aria-describedby="chart-description"
/>
<figcaption id="chart-description">
Q1: 100K, Q2: 150K, Q3: 200K, Q4: 250K
</figcaption>
</figure>
Цвета и контраст
/* === Минимальный контраст: 4.5:1 для текста === */
/* ХОРОШО: тёмный текст на светлом фоне */
.container {
color: #000000; /* чёрный */
background: #FFFFFF; /* белый */
/* контраст: 21:1 */
}
/* ПЛОХО: слабый контраст */
.container {
color: #999999; /* серый */
background: #EEEEEE; /* светло-серый */
/* контраст: 1.2:1 — не достаточно */
}
/* === Не полагайся ТОЛЬКО на цвет === */
/* НЕПРАВИЛЬНО: различие только по цвету */
<div className="text-red">Error</div>
/* ПРАВИЛЬНО: цвет + текст + иконка */
<div className="flex gap-2">
<ErrorIcon />
<span className="text-red">Error message</span>
</div>
Фокус и навигация клавиатуры
// === Видимый фокус ===
export function AccessibleButton() {
return (
<button
className="px-4 py-2 border-2 border-transparent
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2"
>
Click me
</button>
// focus:ring-2 — видимое кольцо при фокусе
);
}
// === Правильный порядок табулирования ===
export function FormWithTabOrder() {
return (
<form>
<input tabIndex={1} placeholder="First" />
<input tabIndex={2} placeholder="Second" />
<button tabIndex={3}>Submit</button>
{/* Порядок: 1, 2, 3 */}
</form>
);
}
// === Скрытие элемента от табулирования ===
<button tabIndex={-1}>Hidden from tab order</button>
<div role="presentation" tabIndex={-1}>Decorative</div>