TypeScript и Accessibility: как писать код, доступный каждому

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

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

Начать курс

Почему Accessibility — это не про "галочки", а про людей

Давайте честно: когда мы слышим "accessibility" (или a11y), многие из нас представляют бесконечные списки требований, скучные проверки и фразу "ну, это для людей с ограничениями". Но правда в том, что доступность — это про каждого. Вы когда-нибудь пытались заказать пиццу на ходу, держа телефон одной рукой? Или сидели в метро с плохим интернетом, где сайт грузится битый час? Вот это и есть проблемы доступности.

В этой статье мы поговорим о том, как TypeScript может стать вашим лучшим другом в создании доступных веб-приложений. Не просто "добавить теги", а построить архитектуру, где инклюзивность — это не фича, а фундамент.

TypeScript как щит от a11y-ошибок

Многие думают, что TypeScript — это только про типы данных и защиту от багов. Но на самом деле, строгая типизация может спасать и в вопросах доступности. Давайте разберем на примерах.

Типизация ARIA-атрибутов

Одна из самых частых проблем — неправильные значения ARIA-атрибутов. Вы когда-нибудь писали role="button" на div? А потом забывали добавить tabindex? TypeScript может отловить это на этапе компиляции.

// Плохо: никакой защиты
function createButton(label: string) {
  return `<div role="button">${label}</div>`;
}

// Хорошо: типизируем ARIA
type AriaRole = 'button' | 'navigation' | 'alert' | 'dialog';

interface AriaAttributes {
  role: AriaRole;
  'aria-label'?: string;
  'aria-expanded'?: boolean;
  tabIndex?: number;
}

function createAccessibleButton(label: string, aria: AriaAttributes) {
  // Теперь TypeScript проверит, что роль — валидная
  return {
    tag: 'div',
    attrs: aria,
    content: label
  };
}

// Ошибка! 'btn' не входит в тип AriaRole
// createAccessibleButton('Нажми меня', { role: 'btn' });

Чувствуете разницу? TypeScript не даст вам случайно написать role="btutton" или забыть обязательный атрибут. Это как персональный ассистент, который шепчет: "Эй, а ты уверен, что это доступно?"

Строгая проверка фокусных состояний

Еще одна боль — управление фокусом. Особенно в SPA, где страница не перезагружается. TypeScript помогает создать безопасные обертки.

// Определяем, какие элементы могут получать фокус
type FocusableElement = HTMLInputElement | HTMLButtonElement | HTMLAnchorElement | HTMLSelectElement | HTMLTextAreaElement;

function safelySetFocus(element: FocusableElement | null): void {
  if (element && document.contains(element)) {
    element.focus();
  } else {
    console.warn('Элемент не найден в DOM или не может получить фокус');
  }
}

// Использование
const modal = document.querySelector('.modal__close-btn');
safelySetFocus(modal as FocusableElement | null);

Обратите внимание: мы не просто вызываем .focus(), а проверяем, существует ли элемент и может ли он быть сфокусирован. Это предотвращает ошибки, которые могут оставить пользователя скринридера в пустоте.

Строим архитектуру с a11y в основе

Теперь перейдем к более продвинутым вещам. Как сделать так, чтобы accessibility была вшита в архитектуру вашего проекта, а не добавлена "на коленке" перед релизом?

Типизированные пропсы для компонентов

Создайте базовый интерфейс для всех ваших компонентов. Это как конституция для доступности.

// Базовый интерфейс для всех UI-компонентов
interface AccessibleComponentProps {
  id?: string;
  className?: string;
  ariaLabel?: string;
  ariaDescribedby?: string;
  role?: string;
  tabIndex?: number;
  isDisabled?: boolean;
  onClick?: () => void;
  onKeyDown?: (e: React.KeyboardEvent) => void;
}

// Конкретный компонент кнопки
interface ButtonProps extends AccessibleComponentProps {
  variant: 'primary' | 'secondary';
  size: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
}

function AccessibleButton(props: ButtonProps) {
  const { ariaLabel, children, isDisabled, onClick, ...rest } = props;

  return (
    <button
      {...rest}
      aria-label={ariaLabel || (typeof children === 'string' ? children : undefined)}
      disabled={isDisabled}
      onClick={!isDisabled ? onClick : undefined}
    >
      {children}
    </button>
  );
}

Зачем это? Теперь каждый новый компонент автоматически "наследует" требования к доступности. Вы не сможете создать кнопку без возможности добавить aria-label — TypeScript напомнит.

Управление состоянием для скринридеров

Часто мы скрываем контент, но забываем сказать об этом скринридеру. TypeScript поможет создать систему оповещений.

// Типы для состояний доступности
type LiveRegion = 'polite' | 'assertive' | 'off';

interface AriaLiveMessage {
  message: string;
  priority: LiveRegion;
  duration?: number;
}

class AccessibilityManager {
  private liveRegion: HTMLDivElement | null = null;

  constructor() {
    this.createLiveRegion();
  }

  private createLiveRegion(): void {
    this.liveRegion = document.createElement('div');
    this.liveRegion.setAttribute('aria-live', 'polite');
    this.liveRegion.setAttribute('aria-atomic', 'true');
    this.liveRegion.className = 'sr-only';
    document.body.appendChild(this.liveRegion);
  }

  announce(message: string, priority: LiveRegion = 'polite'): void {
    if (this.liveRegion) {
      this.liveRegion.setAttribute('aria-live', priority);
      this.liveRegion.textContent = ''; // Очищаем для повторного объявления
      setTimeout(() => {
        this.liveRegion!.textContent = message;
      }, 100);
    }
  }
}

// Использование
const a11y = new AccessibilityManager();
a11y.announce('Форма успешно отправлена', 'assertive');

Это не просто код — это забота о пользователе. Представьте, что вы не видите экран, а просто слышите голос. Такая система сообщит вам о важных изменениях на странице, не заставляя гадать "а что там произошло?".

Реальные кейсы: где TypeScript спасает доступность

Давайте разберем три ситуации, с которыми сталкивается каждый разработчик.

1. Динамические списки и навигация с клавиатуры

interface ListItem {
  id: string;
  label: string;
  isActive: boolean;
}

function AccessibleList({ items }: { items: ListItem[] }) {
  const [focusedIndex, setFocusedIndex] = useState(-1);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch(e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setFocusedIndex(prev => Math.min(prev + 1, items.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setFocusedIndex(prev => Math.max(prev - 1, 0));
        break;
      case 'Enter':
      case ' ':                

Блоги

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