Accessibility в JavaScript: как сделать сайт удобным для всех

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Accessibility в JavaScript: как сделать сайт удобным для всех

Представьте, что вы заходите на сайт, а кнопка «Купить» не нажимается мышкой — только с клавиатуры. Или вы не видите экран, а скринридер просто пропускает половину контента. Неприятно, правда? А ведь для миллионов людей это — ежедневная реальность.

Accessibility (или a11y) — это не просто модный термин. Это способ сделать ваш код честным. Доступность в JavaScript — это про то, чтобы ваш сайт работал для всех: для людей с ограниченным зрением, с моторными нарушениями, для тех, кто пользуется только клавиатурой или голосовым управлением. И да, это ещё и про SEO: поисковики любят чистый, семантически правильный код.

В этой статье мы разберём реальные примеры на JavaScript, которые можно внедрить уже сегодня. Никакой воды — только код и практика.

Почему accessibility — это ваша ответственность?

Если вы пишете на JavaScript, вы управляете поведением интерфейса. Именно JS чаще всего ломает доступность: динамические модалки, бесконечные скроллы, кастомные селекты. Скринридеры (программы чтения с экрана) не умеют читать ваши мысли — они читают то, что вы им скажете через HTML и ARIA.

Хорошая новость: исправить большинство проблем можно за вечер. Плохая: если не думать об accessibility на старте, переписывать придётся неделями.

ARIA-атрибуты: ваш главный инструмент

ARIA (Accessible Rich Internet Applications) — это набор атрибутов, которые помогают вспомогательным технологиям понять, что происходит на странице. JavaScript идеально подходит для того, чтобы динамически обновлять эти атрибуты.

Пример 1: Динамическое обновление aria-live

Допустим, у вас есть чат, где сообщения подгружаются без перезагрузки. Скринридер должен объявить новое сообщение. Без aria-live пользователь просто не узнает об обновлении.

// Создаём контейнер для уведомлений
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.classList.add('sr-only'); // скрываем визуально
document.body.appendChild(liveRegion);

// Функция добавления сообщения
function addChatMessage(message) {
  liveRegion.textContent = ''; // очищаем
  liveRegion.textContent = `Новое сообщение: ${message}`;
}

// Пример вызова
addChatMessage('Привет! Как дела?');

Важно: aria-live="polite" говорит скринридеру: «Объяви, когда закончишь читать текущее». aria-live="assertive" прерывает чтение — используйте для критичных ошибок.

Пример 2: Кастомный спойлер/аккордеон

Стандартные <details> и <summary> уже доступны, но если вы делаете кастомный аккордеон на JS, нужно добавить ARIA-роли и состояния.

// Для каждой секции аккордеона
const accordionButton = document.querySelector('.accordion-button');
const accordionPanel = document.querySelector('.accordion-panel');

accordionButton.setAttribute('aria-expanded', 'false');
accordionButton.setAttribute('aria-controls', 'panel-1');
accordionPanel.setAttribute('id', 'panel-1');
accordionPanel.setAttribute('aria-hidden', 'true');

accordionButton.addEventListener('click', () => {
  const isOpen = accordionButton.getAttribute('aria-expanded') === 'true';
  accordionButton.setAttribute('aria-expanded', !isOpen);
  accordionPanel.setAttribute('aria-hidden', isOpen);
});

Теперь скринридер понимает, что кнопка управляет панелью, и знает, открыта она или закрыта.

Управление фокусом: не дайте пользователю потеряться

Одна из самых частых ошибок в JavaScript доступности — потеря фокуса. Когда открывается модальное окно, фокус должен переместиться внутрь. Когда закрывается — вернуться обратно на кнопку, которая его вызвала.

Пример: Модальное окно с правильным фокусом

function openModal(modalId) {
  const modal = document.getElementById(modalId);
  const lastFocused = document.activeElement; // запоминаем, где был фокус

  modal.classList.add('modal--open');
  modal.setAttribute('aria-hidden', 'false');

  // Перемещаем фокус на первый элемент внутри модалки
  const firstFocusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  if (firstFocusable) {
    firstFocusable.focus();
  }

  // Сохраняем lastFocused для закрытия
  modal.dataset.lastFocused = lastFocused.id || '';

  // Обработчик закрытия
  modal.querySelector('.modal-close').addEventListener('click', () => {
    closeModal(modalId);
  });
}

function closeModal(modalId) {
  const modal = document.getElementById(modalId);
  modal.classList.remove('modal--open');
  modal.setAttribute('aria-hidden', 'true');

  // Возвращаем фокус
  const lastFocusedId = modal.dataset.lastFocused;
  if (lastFocusedId) {
    document.getElementById(lastFocusedId).focus();
  }
}

Этот код — база. Для продакшена добавьте ещё обработку Escape и ловушку фокуса (чтобы нельзя было выйти за пределы модалки табуляцией).

Клавиатурная навигация: не только для Tab

Многие пользователи не используют мышь. Они перемещаются по сайту с помощью клавиш Tab, Enter, пробела и стрелок. Ваша задача — сделать так, чтобы все интерактивные элементы были доступны с клавиатуры.

Пример: Кастомный select (выпадающий список)

class AccessibleSelect {
  constructor(container) {
    this.container = container;
    this.button = container.querySelector('.custom-select__button');
    this.list = container.querySelector('.custom-select__list');
    this.items = container.querySelectorAll('.custom-select__item');
    this.currentIndex = -1;

    this.button.addEventListener('keydown', (e) => this.handleButtonKeydown(e));
    this.list.addEventListener('keydown', (e) => this.handleListKeydown(e));
    this.items.forEach((item, index) => {
      item.addEventListener('click', () => this.selectItem(index));
    });
  }

  handleButtonKeydown(e) {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      this.toggleList();
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      this.openList();
      this.focusItem(0);
    }
  }

  handleListKeydown(e) {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      this.focusItem(this.currentIndex + 1);
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      this.focusItem(this.currentIndex - 1);
    }
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      this.selectItem(this.currentIndex);
    }
    if (e.key === 'Escape') {
      this.closeList();
      this.button.focus();
    }
  }

  focusItem(index) {
    if (index < 0) index = this.items.length - 1;
    if (index >= this.items.length) index = 0;
    this.currentIndex = index;
    this.items[index].focus();
  }

  selectItem(index) {
    this.button.textContent = this.items[index].textContent;
    this.closeList();                

Блоги

Рекомендаций по книгам