← Назад к вопросам

Как реализуется доступность сайта в верстке?

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. Это значит что не нужно добавлять её в конце - нужно строить с самого начала.

Простой способ помнить: если бы ты пользовался сайтом:

  • Только с клавиатурой (без мышки)
  • С закрытыми глазами (скринридер)
  • На мобильном телефоне (маленький экран)

Все ли работало бы? Если да - сайт доступен.

Как реализуется доступность сайта в верстке? | PrepBro