Назад к блогу
Frontend

Доступность веб-сайтов (a11y): полное руководство

Создание доступных веб-сайтов: ARIA атрибуты, семантика, фокус, контраст, screen readers и WCAG стандарты.

5 января 2026 г.
12 мин чтения
59 просмотров
MOLOTILO

MOLOTILO DIGITAL

Доступность веб-сайтов (a11y): полное руководство

Почему доступность важна

Доступность (accessibility, a11y) — это практика создания сайтов, которыми могут пользоваться все люди, включая людей с ограниченными возможностями. Это не только этично, но и расширяет аудиторию и улучшает SEO.

Семантическая вёрстка

Правильные HTML теги — основа доступности:

<!-- ❌ Плохо -->
<div class="header">
  <div class="nav">
    <div class="link" onclick="...">Главная</div>
  </div>
</div>

<!-- ✅ Хорошо -->
<header>
  <nav aria-label="Главная навигация">
    <a href="/">Главная</a>
    <a href="/about">О нас</a>
  </nav>
</header>

<main>
  <article>
    <h1>Заголовок страницы</h1>
    <section>
      <h2>Подзаголовок</h2>
      <p>Контент...</p>
    </section>
  </article>
</main>

<footer>
  <nav aria-label="Дополнительная навигация">...</nav>
</footer>

ARIA атрибуты

// Кнопка-иконка
<button aria-label="Закрыть модальное окно">
  <XIcon />
</button>

// Модальное окно
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
>
  <h2 id="modal-title">Подтверждение</h2>
  <p id="modal-description">Вы уверены?</p>
</div>

// Состояние загрузки
<button aria-busy={isLoading} disabled={isLoading}>
  {isLoading ? 'Загрузка...' : 'Отправить'}
</button>

// Уведомления
<div role="alert" aria-live="polite">
  Форма успешно отправлена
</div>

// Навигация с текущей страницей
<nav>
  <a href="/" aria-current="page">Главная</a>
  <a href="/about">О нас</a>
</nav>

// Раскрывающееся меню
<button
  aria-expanded={isOpen}
  aria-controls="dropdown-menu"
  aria-haspopup="true"
>
  Меню
</button>
<ul id="dropdown-menu" role="menu" hidden={!isOpen}>
  <li role="menuitem"><a href="#">Пункт 1</a></li>
</ul>

Управление фокусом

// Фокус-ловушка для модального окна
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement>();

  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  // Ловушка фокуса
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Tab') {
      const focusable = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      if (!focusable?.length) return;
      
      const first = focusable[0] as HTMLElement;
      const last = focusable[focusable.length - 1] as HTMLElement;
      
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
    
    if (e.key === 'Escape') {
      onClose();
    }
  };

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}

// Skip link для клавиатурной навигации
<a href="#main-content" className="skip-link">
  Перейти к основному содержимому
</a>

// CSS для skip link
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  padding: 8px;
  background: #000;
  color: #fff;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

Контраст и цвета

/* WCAG AA требует контраст 4.5:1 для текста */
/* WCAG AAA требует контраст 7:1 */

/* ❌ Плохо — низкий контраст */
.text-low-contrast {
  color: #999;
  background: #fff;
}

/* ✅ Хорошо — достаточный контраст */
.text-good-contrast {
  color: #595959;
  background: #fff;
}

/* Не полагайтесь только на цвет */
/* ❌ Плохо */
.error { color: red; }

/* ✅ Хорошо — цвет + иконка + текст */
.error {
  color: #dc2626;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.error::before {
  content: '⚠️';
}

/* Фокус должен быть видимым */
:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* Не убирайте outline без замены */
:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Формы

// Связь label и input
<label htmlFor="email">Email</label>
<input id="email" type="email" required />

// Описание ошибок
<div>
  <label htmlFor="password">Пароль</label>
  <input
    id="password"
    type="password"
    aria-describedby="password-error password-hint"
    aria-invalid={!!error}
  />
  <p id="password-hint">Минимум 8 символов</p>
  {error && <p id="password-error" role="alert">{error}</p>}
</div>

// Группировка полей
<fieldset>
  <legend>Способ доставки</legend>
  <label>
    <input type="radio" name="delivery" value="pickup" />
    Самовывоз
  </label>
  <label>
    <input type="radio" name="delivery" value="courier" />
    Курьер
  </label>
</fieldset>

Изображения

// Информативное изображение
<img src="/product.jpg" alt="Красный рюкзак с двумя карманами" />

// Декоративное изображение
<img src="/decoration.svg" alt="" role="presentation" />

// Сложное изображение (график)
<figure>
  <img
    src="/chart.png"
    alt="График продаж за 2024 год"
    aria-describedby="chart-description"
  />
  <figcaption id="chart-description">
    Продажи выросли на 25% в Q4 по сравнению с Q1
  </figcaption>
</figure>

Тестирование доступности

# Инструменты
# - axe DevTools (расширение браузера)
# - Lighthouse (встроен в Chrome)
# - WAVE (wave.webaim.org)
# - Screen readers: NVDA (Windows), VoiceOver (Mac)

# Автоматические тесты
npm install @axe-core/playwright
// Playwright тест
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage should have no accessibility violations', async ({ page }) => {
  await page.goto('/');
  
  const results = await new AxeBuilder({ page }).analyze();
  
  expect(results.violations).toEqual([]);
});

Заключение

Доступность — это не дополнительная функция, а базовое требование. Семантическая вёрстка, ARIA атрибуты, управление фокусом и достаточный контраст делают сайт доступным для всех. Тестируйте с screen readers и автоматическими инструментами.

Доступный сайт — это хороший сайт. Практики a11y улучшают UX для всех пользователей.

ДоступностьA11yARIAWCAGUX

Понравилась статья?

Подпишитесь на наш блог, чтобы не пропустить новые материалы