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

Как реализуется доступность 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>
Как реализуется доступность HTLM? | PrepBro