← Назад к вопросам
Как обеспечить корректную работу скрин-ридеров на сайте?
1.3 Junior🔥 81 комментариев
#Soft Skills и рабочие процессы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Accessibility (a11y) и поддержка скрин-ридеров
Скрин-ридеры - это критически важные инструменты для людей с нарушениями зрения. Обеспечение полной доступности - это не только этическое требование, но и часть современного веб-стандарта WCAG 2.1.
1. Семантический HTML
Правильное использование элементов
<!-- Неправильно: всё на div'ах -->
<div class="button" onclick="handleClick()">Отправить</div>
<div class="heading">Заголовок</div>
<div class="list">
<div>Элемент 1</div>
<div>Элемент 2</div>
</div>
<!-- Правильно: семантические элементы -->
<button onClick={handleClick}>Отправить</button>
<h1>Заголовок</h1>
<ul>
<li>Элемент 1</li>
<li>Элемент 2</li>
</ul>
<nav> {/* Навигация */}
<main> {/* Главное содержимое */}
<article> {/* Статья */}
<section> {/* Раздел */}
<aside> {/* Боковая панель */}
<footer> {/* Подвал */}
2. ARIA атрибуты
// aria-label - описание для элементов без текста
<button aria-label="Закрыть модальное окно">
✕
</button>
// aria-describedby - расширенное описание
<input type="password" aria-describedby="pwd-hint" />
<p id="pwd-hint">Минимум 8 символов, включая буквы и цифры</p>
// aria-live - динамические обновления контента
<div aria-live="polite" aria-atomic="true">
{message} {/* При изменении скрин-ридер объявит новое значение */}
</div>
// aria-hidden - скрыть от скрин-ридера
<span aria-hidden="true">→</span> {/* Декоративный элемент */}
// role - определить роль элемента
<div role="alert">Критическая ошибка!</div>
// aria-expanded/aria-controls
<button
aria-expanded={isOpen}
aria-controls="menu"
>
Меню
</button>
<ul id="menu" hidden={!isOpen}>
<li>Опция 1</li>
</ul>
3. Компонент доступного Button
interface AccessibleButtonProps {
children: React.ReactNode;
onClick: () => void;
disabled?: boolean;
ariaLabel?: string;
ariaDescribedBy?: string;
}
export function AccessibleButton({
children,
onClick,
disabled = false,
ariaLabel,
ariaDescribedBy,
}: AccessibleButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel}
aria-describedby={ariaDescribedBy}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{children}
</button>
);
}
4. Навигация с клавиатуры
export function AccessibleMenu() {
const [focusedIndex, setFocusedIndex] = useState(0);
const items = ['Главная', 'О нас', 'Контакты'];
const refs = useRef<HTMLButtonElement[]>([]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextIndex = (focusedIndex + 1) % items.length;
setFocusedIndex(nextIndex);
refs.current[nextIndex]?.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prevIndex = focusedIndex === 0 ? items.length - 1 : focusedIndex - 1;
setFocusedIndex(prevIndex);
refs.current[prevIndex]?.focus();
} else if (e.key === 'Home') {
e.preventDefault();
setFocusedIndex(0);
refs.current[0]?.focus();
}
};
return (
<ul
role="menubar"
onKeyDown={handleKeyDown}
className="flex gap-4"
>
{items.map((item, index) => (
<li key={item} role="none">
<button
ref={(el) => { if (el) refs.current[index] = el; }}
role="menuitem"
tabIndex={focusedIndex === index ? 0 : -1}
className="px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{item}
</button>
</li>
))}
</ul>
);
}
5. Форма с полной доступностью
export function AccessibleForm() {
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
if (!value.includes('@')) {
setEmailError('Email должен содержать @');
} else {
setEmailError('');
}
};
return (
<form className="space-y-4">
<div className="flex flex-col">
<label htmlFor="email" className="font-medium mb-2">
Email
<span aria-label="обязательное поле" className="text-red-600">*</span>
</label>
<input
id="email"
type="email"
value={email}
onChange={handleEmailChange}
aria-required="true"
aria-invalid={!!emailError}
aria-describedby={emailError ? 'email-error' : undefined}
className="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
{emailError && (
<p id="email-error" className="text-red-600 text-sm mt-1" role="alert">
{emailError}
</p>
)}
</div>
<button
type="submit"
aria-label="Отправить форму"
className="px-4 py-2 bg-blue-600 text-white rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Отправить
</button>
</form>
);
}
6. Изображения и альтернативный текст
// Неправильно
<img src="photo.jpg" />
// Правильно
<img
src="photo.jpg"
alt="Портрет человека в офисе, сидящего за компьютером"
/>
// Для декоративных изображений
<img src="divider.png" alt="" aria-hidden="true" />
// Для сложных изображений (графики, диаграммы)
<figure>
<img src="chart.png" alt="Диаграмма продаж по месяцам" />
<figcaption>Продажи выросли на 40% в марте</figcaption>
</figure>
7. Цвет и контрастность
/* Минимальный контраст 4.5:1 для текста */
.text-dark {
color: #1f2937; /* Почти черный */
background: #ffffff; /* Белый */
/* Контраст: 16.43:1 - отлично */
}
/* Не полагайся только на цвет */
.error-field {
border: 2px solid #dc2626; /* Красный */
border-left: 4px solid #dc2626; /* Дополнительный индикатор */
background-color: #fee2e2;
}
.success {
border: 2px solid #16a34a; /* Зеленый */
/* + иконка галочки для дублирования информации */
}
8. Использование heading иерархии
// Правильно
export function BlogPost() {
return (
<article>
<h1>Главный заголовок статьи</h1> {/* Одна h1 на странице */}
<section>
<h2>Раздел 1</h2>
<h3>Подраздел 1.1</h3>
<p>Текст...</p>
</section>
<section>
<h2>Раздел 2</h2>
<h3>Подраздел 2.1</h3>
<p>Текст...</p>
</section>
</article>
);
}
// Неправильно - нарушена иерархия
<h1>Заголовок</h1>
<h3>Подраздел</h3> {/* Пропущена h2 */}
9. Тестирование доступности
# Установка инструментов
npm install --save-dev @testing-library/jest-dom @axe-core/react
# Тест с axe
import { axe, toHaveNoViolations } from 'jest-axe';
test('Button should have no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Чеклист доступности
- Используйте семантический HTML
- Все интерактивные элементы должны быть доступны с клавиатуры
- Текст должен иметь контраст минимум 4.5:1
- Все изображения должны иметь alt текст
- Иерархия heading правильная (h1 -> h2 -> h3)
- Форма имеет правильные label'ы
- Используйте aria-live для динамического контента
- Тестируйте с реальными скрин-ридерами (NVDA, JAWS, VoiceOver)
Доступность - это не фича, это основание хорошего веб-приложения.