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

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

2.0 Middle🔥 111 комментариев
#JavaScript Core#Браузер и сетевые технологии

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Web Accessibility в JavaScript

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

WCAG 2.1 стандарты

Все рекомендации основаны на WCAG 2.1 (Web Content Accessibility Guidelines), которые определяют три уровня соответствия:

  • A - минимальный уровень
  • AA - рекомендуемый уровень
  • AAA - максимальный уровень

Большинство проектов стремятся к уровню AA.

Семантический HTML - основание

Доступность начинается с правильного HTML, а JavaScript работает с ним:

<!-- ❌ Плохо - не семантично -->
<div onclick="handleClick()" role="button">
  Кнопка
</div>

<!-- ✅ Хорошо - семантично -->
<button onclick="handleClick()">
  Кнопка
</button>

<!-- ❌ Плохо - нет метки -->
<input type="text" />

<!-- ✅ Хорошо - есть метка -->
<label for="username">Имя пользователя</label>
<input id="username" type="text" />

ARIA атрибуты

ARIA (Accessible Rich Internet Applications) позволяет добавить информацию доступности к элементам, которые не имеют встроенной семантики.

Атрибут role

// Предоставляет информацию скрин-ридерам о роли элемента
<div role="alert" aria-live="assertive">
  Ошибка: неверный email
</div>

<div role="navigation" aria-label="Основная навигация">
  // навигация
</div>

<div role="tablist">
  <button role="tab" aria-selected="true">Таб 1</button>
  <button role="tab" aria-selected="false">Таб 2</button>
</div>

Атрибут aria-label

// Предоставляет текстовое описание элемента
<button aria-label="Закрыть диалог" onclick="closeDialog()">
  <span aria-hidden="true">&times;</span>
</button>

// Полезно для иконок без текста
<button aria-label="Поиск" class="btn-search">
  <svg><!-- иконка поиска --></svg>
</button>

Атрибут aria-live

// Уведомляет скрин-ридер об изменениях в режиме реального времени
<div aria-live="polite" aria-atomic="true">
  Загрузка...
</div>

// Варианты aria-live:
// - off: не объявлять
// - polite: объявить когда пользователь свободен
// - assertive: объявить немедленно

Атрибут aria-describedby

<input 
  type="password" 
  aria-describedby="pwd-hint"
/>
<span id="pwd-hint">
  Минимум 8 символов, содержащих букву и цифру
</span>

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

Использователи с моторными нарушениями полагаются на навигацию с клавиатуры. JavaScript должен обеспечить нормальную работу Tab, Enter, Escape, стрелок.

// Правильно: используем встроенные элементы
<button>Кнопка</button> // Автоматически focusable
<input type="text" /> // Автоматически focusable
<a href="#">Ссылка</a> // Автоматически focusable

// Если нужен кастомный элемент, сделай его focusable
const customButton = document.createElement('div');
customButton.tabIndex = 0; // Добавляет элемент в порядок табуляции
customButton.role = 'button';

customButton.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    handleClick();
  }
});

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

// Важно управлять фокусом при открытии модального диалога
class Modal {
  open() {
    this.dialog.showModal();
    // Переместить фокус в диалог
    this.firstFocusableElement.focus();
  }
  
  close() {
    this.dialog.close();
    // Вернуть фокус на элемент, который открыл диалог
    this.triggerButton.focus();
  }
  
  // Ловушка фокуса: удерживает фокус внутри диалога при Tab
  trapFocus(e) {
    if (e.key !== 'Tab') return;
    
    const focusableElements = this.dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];
    
    if (e.shiftKey) {
      if (document.activeElement === firstElement) {
        lastElement.focus();
        e.preventDefault();
      }
    } else {
      if (document.activeElement === lastElement) {
        firstElement.focus();
        e.preventDefault();
      }
    }
  }
}

Скрин-ридеры и объявления

// Объявить результат поиска для скрин-ридера
function performSearch(query) {
  const results = searchAPI.search(query);
  
  // Обновляем страницу
  renderResults(results);
  
  // Объявляем результат
  const announcement = `Найдено ${results.length} результатов`;
  announceToScreenReader(announcement);
}

function announceToScreenReader(message) {
  const announcer = document.getElementById('sr-announcer');
  announcer.textContent = message;
  
  // Скрин-ридер объявит это сообщение из-за aria-live
}

HTML для объявлений:

<!-- Скрытый элемент для объявлений скрин-ридерам -->
<div 
  id="sr-announcer" 
  aria-live="polite" 
  aria-atomic="true"
  class="sr-only"
></div>

<style>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
</style>

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

// JavaScript может проверять контрастность
function getContrastRatio(color1, color2) {
  const lum1 = getRelativeLuminance(color1);
  const lum2 = getRelativeLuminance(color2);
  
  const lighter = Math.max(lum1, lum2);
  const darker = Math.min(lum1, lum2);
  
  return (lighter + 0.05) / (darker + 0.05);
}

// Проверяем соответствие WCAG
function isAccessibleContrast(ratio) {
  // AA: 4.5:1 для обычного текста
  // AAA: 7:1 для обычного текста
  return ratio >= 4.5;
}

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

// Использование инструментов для проверки
import { axe } from 'jest-axe';
import { render } from '@testing-library/react';

test('компонент доступен', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

// Ручная проверка:
// 1. Навигация только с клавиатуры (Tab, Enter, Escape)
// 2. Скрин-ридер (NVDA, JAWS, VoiceOver)
// 3. Zoom до 200%
// 4. Высокий контраст
// 5. Темный режим

Практический пример: Доступный dropdown

class AccessibleDropdown {
  constructor(trigger, menu) {
    this.trigger = trigger;
    this.menu = menu;
    this.items = menu.querySelectorAll('[role="menuitem"]');
    this.isOpen = false;
    
    this.init();
  }
  
  init() {
    this.trigger.setAttribute('aria-haspopup', 'menu');
    this.trigger.setAttribute('aria-expanded', 'false');
    this.menu.setAttribute('role', 'menu');
    
    this.trigger.addEventListener('click', () => this.toggle());
    this.trigger.addEventListener('keydown', (e) => this.handleTriggerKey(e));
    this.menu.addEventListener('keydown', (e) => this.handleMenuKey(e));
  }
  
  toggle() {
    this.isOpen ? this.close() : this.open();
  }
  
  open() {
    this.isOpen = true;
    this.menu.hidden = false;
    this.trigger.setAttribute('aria-expanded', 'true');
    this.items[0].focus();
  }
  
  close() {
    this.isOpen = false;
    this.menu.hidden = true;
    this.trigger.setAttribute('aria-expanded', 'false');
    this.trigger.focus();
  }
  
  handleTriggerKey(e) {
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      this.open();
    }
  }
  
  handleMenuKey(e) {
    const currentIndex = Array.from(this.items).indexOf(document.activeElement);
    
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        const nextIndex = (currentIndex + 1) % this.items.length;
        this.items[nextIndex].focus();
        break;
      case 'ArrowUp':
        e.preventDefault();
        const prevIndex = (currentIndex - 1 + this.items.length) % this.items.length;
        this.items[prevIndex].focus();
        break;
      case 'Escape':
        e.preventDefault();
        this.close();
        break;
    }
  }
}

Основные принципы доступности

  1. Воспринимаемость (Perceivable) - информация должна быть видна/слышна
  2. Операбельность (Operable) - интерфейс должен работать с клавиатурой
  3. Понятность (Understandable) - текст должен быть ясным, навигация логичной
  4. Надежность (Robust) - совместимость с вспомогательными технологиями

Итог

Доступность - это не дополнительный feature, а обязательная часть качественного веб-приложения. JavaScript должен:

  • Сохранять семантику HTML
  • Предоставлять ARIA информацию где необходимо
  • Обеспечивать навигацию с клавиатуры
  • Объявлять динамические изменения скрин-ридерам
  • Управлять фокусом в интерактивных компонентах

Правильная реализация доступности помогает всем пользователям, включая людей с ограничениями.