12k
All articles

Советы по улучшению навигации с клавиатуры в веб-приложениях

Исправление порядка Tab, захват фокуса в модальных окнах и ARIA-атрибуты с семантическим HTML делают веб-приложения доступными с клавиатуры.

OpenReplay Team
OpenReplay Team
Советы по улучшению навигации с клавиатуры в веб-приложениях

Создание веб-приложений, доступных для навигации с клавиатуры, — это не просто вопрос соответствия стандартам, а создание интерфейсов, которые работают для всех. Тем не менее, многие разработчики сталкиваются с проблемами управления фокусом, нарушенными последовательностями табуляции и недоступными пользовательскими компонентами. Это руководство предоставляет практические решения для распространенных проблем доступности навигации с клавиатуры, с которыми вы столкнетесь в реальной разработке.

Ключевые выводы

  • Структурируйте DOM в соответствии с визуальным порядком табуляции, а не CSS-макетом
  • Используйте семантические HTML-элементы для встроенной поддержки клавиатуры
  • Никогда не удаляйте индикаторы фокуса без предоставления пользовательских альтернатив
  • Реализуйте захват фокуса для модальных диалогов и восстанавливайте фокус при закрытии
  • Тестируйте навигацию с клавиатуры вручную и с помощью автоматизированных инструментов
  • Используйте tabindex="0" для пользовательских интерактивных элементов, избегайте положительных значений

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

Проблема порядка табуляции

Наиболее критичным аспектом доступности навигации с клавиатуры является установление логичного порядка табуляции. Структура вашего DOM напрямую определяет последовательность фокуса, а не CSS-макет. Это несоответствие вызывает серьезные проблемы с удобством использования.

Распространенная ошибка:

<!-- Визуальный порядок: Логотип, Навигация, Контент, Боковая панель -->
<div class="layout">
  <div class="sidebar">...</div>  <!-- Фокус первым -->
  <div class="content">...</div>  <!-- Фокус вторым -->
  <nav class="navigation">...</nav> <!-- Фокус третьим -->
  <div class="logo">...</div>     <!-- Фокус последним -->
</div>

Лучший подход:

<!-- Порядок DOM соответствует визуальному потоку -->
<div class="layout">
  <div class="logo">...</div>
  <nav class="navigation">...</nav>
  <div class="content">...</div>
  <div class="sidebar">...</div>
</div>

Используйте CSS Grid или Flexbox для управления визуальным позиционированием, сохраняя при этом логичный порядок DOM.

Семантические HTML-элементы для лучшей навигации

Нативные HTML-элементы обеспечивают встроенную поддержку клавиатуры. Используйте их вместо воссоздания функциональности с помощью div и span.

Интерактивные элементы, которые работают из коробки:

  • <button> для действий
  • <a href="..."> для навигации
  • <input>, <select>, <textarea> для элементов форм
  • <details> и <summary> для раскрывающегося контента

Избегайте этого паттерна:

<div class="button" onclick="handleClick()">Отправить</div>

Используйте это вместо:

<button type="button" onclick="handleClick()">Отправить</button>

Когда нативный HTML не может обеспечить требуемое поведение, используйте ARIA-атрибуты для добавления семантики доступности — но всегда отдавайте предпочтение семантическим элементам в первую очередь.

Сохранение видимых стилей фокуса

Кризис индикаторов фокуса

Многие разработчики удаляют индикаторы фокуса с помощью outline: none без предоставления альтернатив. Это полностью нарушает доступность навигации с клавиатуры.

Никогда не делайте этого без замены:

button:focus {
  outline: none; /* Удаляет индикатор фокуса */
}

Предоставьте пользовательские стили фокуса:

button:focus {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

/* Или используйте focus-visible для лучшего UX */
button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

Современное управление фокусом с :focus-visible

Псевдокласс :focus-visible показывает индикаторы фокуса только при обнаружении навигации с клавиатуры, улучшая опыт как для пользователей клавиатуры, так и для пользователей мыши.

/* Базовые стили */
.interactive-element {
  outline: none;
}

/* Только фокус с клавиатуры */
.interactive-element:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
}

Избежание распространенных ошибок с tabindex

Ловушка tabindex

Использование значений tabindex больше 0 создает запутанные паттерны навигации. Придерживайтесь этих трех значений:

  • tabindex="0" - Делает элемент фокусируемым в естественном порядке табуляции
  • tabindex="-1" - Делает элемент программно фокусируемым, но исключает из порядка табуляции
  • Без tabindex - Использует поведение по умолчанию

Проблематичный подход:

<div tabindex="1">Первый</div>
<div tabindex="3">Третий</div>
<div tabindex="2">Второй</div>
<button>Четвертый (естественный порядок)</button>

Лучшее решение:

<div tabindex="0">Первый</div>
<div tabindex="0">Второй</div>
<div tabindex="0">Третий</div>
<button>Четвертый</button>

Делаем пользовательские компоненты фокусируемыми

При создании пользовательских интерактивных элементов добавьте tabindex="0" и обработчики событий клавиатуры:

// Пользовательский компонент выпадающего списка
const dropdown = document.querySelector('.custom-dropdown');
dropdown.setAttribute('tabindex', '0');

dropdown.addEventListener('keydown', (e) => {
  switch(e.key) {
    case 'Enter':
    case ' ':
      toggleDropdown();
      break;
    case 'Escape':
      closeDropdown();
      break;
    case 'ArrowDown':
      openDropdown();
      focusFirstOption();
      break;
  }
});

Предотвращение ловушек клавиатуры в модальных окнах

Реализация захвата фокуса

Модальные диалоги должны захватывать фокус, чтобы предотвратить переход пользователей клавиатуры к фоновому контенту. Вот надежная реализация:

class FocusTrap {
  constructor(element) {
    this.element = element;
    this.focusableElements = this.getFocusableElements();
    this.firstFocusable = this.focusableElements[0];
    this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
  }

  getFocusableElements() {
    const selectors = [
      'button:not([disabled])',
      'input:not([disabled])',
      'select:not([disabled])',
      'textarea:not([disabled])',
      'a[href]',
      '[tabindex]:not([tabindex="-1"])'
    ].join(', ');
    
    return Array.from(this.element.querySelectorAll(selectors));
  }

  activate() {
    this.element.addEventListener('keydown', this.handleKeyDown.bind(this));
    this.firstFocusable?.focus();
  }

  handleKeyDown(e) {
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        if (document.activeElement === this.firstFocusable) {
          e.preventDefault();
          this.lastFocusable.focus();
        }
      } else {
        if (document.activeElement === this.lastFocusable) {
          e.preventDefault();
          this.firstFocusable.focus();
        }
      }
    }
    
    if (e.key === 'Escape') {
      this.deactivate();
    }
  }

  deactivate() {
    this.element.removeEventListener('keydown', this.handleKeyDown);
  }
}

Восстановление фокуса после закрытия модального окна

Всегда возвращайте фокус к элементу, который открыл модальное окно:

let previousFocus;

function openModal() {
  previousFocus = document.activeElement;
  const modal = document.getElementById('modal');
  const focusTrap = new FocusTrap(modal);
  focusTrap.activate();
}

function closeModal() {
  focusTrap.deactivate();
  previousFocus?.focus();
}

Тестирование навигации с клавиатуры

Чек-лист для ручного тестирования

  1. Пройдите табуляцией по всему интерфейсу - Можете ли вы достичь всех интерактивных элементов?
  2. Проверьте индикаторы фокуса - Видны ли они и четкие ли?
  3. Протестируйте модальные диалоги - Работает ли захват фокуса корректно?
  4. Проверьте ссылки для пропуска - Могут ли пользователи обойти повторяющуюся навигацию?
  5. Протестируйте взаимодействие с формами - Работают ли все элементы форм с клавиатурой?

Инструменты тестирования в браузере

Используйте эти инструменты для выявления проблем с навигацией с клавиатуры:

  • axe DevTools - Автоматизированное тестирование доступности
  • WAVE - Оценка веб-доступности
  • Lighthouse - Встроенный аудит доступности Chrome

Интеграция автоматизированного тестирования

Добавьте тесты навигации с клавиатуры в ваш набор тестов:

// Пример с Testing Library
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('модальное окно корректно захватывает фокус', async () => {
  const user = userEvent.setup();
  render(<ModalComponent />);
  
  const openButton = screen.getByText('Открыть модальное окно');
  await user.click(openButton);
  
  const modal = screen.getByRole('dialog');
  const firstButton = screen.getByText('Первая кнопка');
  const lastButton = screen.getByText('Последняя кнопка');
  
  // Фокус должен быть на первом элементе
  expect(firstButton).toHaveFocus();
  
  // Переход к последнему элементу и проверка ловушки
  await user.tab();
  expect(lastButton).toHaveFocus();
  
  await user.tab();
  expect(firstButton).toHaveFocus(); // Должен вернуться к началу
});

Работа со сложными компонентами

Выпадающие меню и комбобоксы

Реализуйте правильную навигацию с клавиатуры для пользовательских выпадающих списков:

class AccessibleDropdown {
  constructor(element) {
    this.dropdown = element;
    this.trigger = element.querySelector('.dropdown-trigger');
    this.menu = element.querySelector('.dropdown-menu');
    this.options = Array.from(element.querySelectorAll('.dropdown-option'));
    this.currentIndex = -1;
    
    this.bindEvents();
  }

  bindEvents() {
    this.trigger.addEventListener('keydown', (e) => {
      switch(e.key) {
        case 'Enter':
        case ' ':
        case 'ArrowDown':
          e.preventDefault();
          this.open();
          break;
      }
    });

    this.menu.addEventListener('keydown', (e) => {
      switch(e.key) {
        case 'ArrowDown':
          e.preventDefault();
          this.focusNext();
          break;
        case 'ArrowUp':
          e.preventDefault();
          this.focusPrevious();
          break;
        case 'Enter':
          this.selectCurrent();
          break;
        case 'Escape':
          this.close();
          break;
      }
    });
  }

  focusNext() {
    this.currentIndex = (this.currentIndex + 1) % this.options.length;
    this.options[this.currentIndex].focus();
  }

  focusPrevious() {
    this.currentIndex = this.currentIndex <= 0 
      ? this.options.length - 1 
      : this.currentIndex - 1;
    this.options[this.currentIndex].focus();
  }
}

Таблицы данных с навигацией с клавиатуры

Большие таблицы данных нуждаются в эффективных паттернах навигации с клавиатуры:

// Roving tabindex для навигации по таблице
class AccessibleTable {
  constructor(table) {
    this.table = table;
    this.cells = Array.from(table.querySelectorAll('td, th'));
    this.currentCell = null;
    this.setupRovingTabindex();
  }

  setupRovingTabindex() {
    this.cells.forEach(cell => {
      cell.setAttribute('tabindex', '-1');
      cell.addEventListener('keydown', this.handleKeyDown.bind(this));
    });
    
    // Первая ячейка получает начальный фокус
    if (this.cells[0]) {
      this.cells[0].setAttribute('tabindex', '0');
      this.currentCell = this.cells[0];
    }
  }

  handleKeyDown(e) {
    const { key } = e;
    let newCell = null;

    switch(key) {
      case 'ArrowRight':
        newCell = this.getNextCell();
        break;
      case 'ArrowLeft':
        newCell = this.getPreviousCell();
        break;
      case 'ArrowDown':
        newCell = this.getCellBelow();
        break;
      case 'ArrowUp':
        newCell = this.getCellAbove();
        break;
    }

    if (newCell) {
      e.preventDefault();
      this.moveFocus(newCell);
    }
  }

  moveFocus(newCell) {
    this.currentCell.setAttribute('tabindex', '-1');
    newCell.setAttribute('tabindex', '0');
    newCell.focus();
    this.currentCell = newCell;
  }
}

Заключение

Эффективная доступность навигации с клавиатуры требует внимания к управлению фокусом, использованию семантического HTML и правильному тестированию. Начните с логичной структуры DOM, сохраняйте индикаторы фокуса, избегайте значений tabindex больше 0 и реализуйте захват фокуса для модальных окон. Регулярное тестирование с фактической навигацией с клавиатуры выявит проблемы, которые могут пропустить автоматизированные инструменты.

Готовы улучшить доступность навигации с клавиатуры в вашем веб-приложении? Начните с аудита вашего текущего интерфейса с помощью клавиши Tab, выявите проблемы управления фокусом и реализуйте паттерны, описанные в этом руководстве. Ваши пользователи будут благодарны за создание более инклюзивного опыта.

Часто задаваемые вопросы

В чем разница между псевдоклассами CSS :focus и :focus-visible?

Псевдокласс :focus применяется всякий раз, когда элемент получает фокус, независимо от того, как он был сфокусирован (мышь, клавиатура или программно). Псевдокласс :focus-visible применяется только тогда, когда браузер определяет, что фокус должен быть видимым, обычно при навигации с клавиатуры. Это позволяет показывать индикаторы фокуса только при необходимости, улучшая опыт для пользователей мыши, сохраняя при этом доступность для пользователей клавиатуры.

Как протестировать навигацию с клавиатуры в разных браузерах?

Используйте ручное тестирование, проходя табуляцией по вашему интерфейсу в Chrome, Firefox, Safari и Edge. Каждый браузер может по-разному обрабатывать фокус. Для автоматизированного тестирования используйте инструменты как axe DevTools, WAVE или Lighthouse. Обратите особое внимание на индикаторы фокуса, поскольку они значительно различаются между браузерами. Рассмотрите использование :focus-visible для согласованного стилирования фокуса в разных браузерах.

Что делать, если мой CSS-макет нарушает логичный порядок табуляции?

Реструктурируйте ваш HTML в соответствии с визуальным потоком, затем используйте CSS Grid или Flexbox для управления позиционированием. Избегайте использования положительных значений tabindex для исправления проблем с порядком табуляции, поскольку это создает больше проблем. Если вы должны использовать CSS для визуального переупорядочивания элементов, убедитесь, что порядок DOM все еще имеет логический смысл для пользователей клавиатуры и программ чтения с экрана.

Как обрабатывать навигацию с клавиатуры в одностраничных приложениях?

Управляйте фокусом при изменении маршрутов, перемещая фокус к основной области контента или заголовку страницы. Используйте библиотеки управления фокусом или реализуйте пользовательское восстановление фокуса. Убедитесь, что обновления динамического контента не нарушают последовательность табуляции, и что вновь добавленные интерактивные элементы правильно фокусируемы. Рассмотрите использование системы управления фокусом, которая отслеживает состояние фокуса при изменении маршрутов.

Почему мои пользовательские компоненты недоступны для пользователей клавиатуры?

Пользовательские компоненты, созданные с элементами div и span, лишены нативной поддержки клавиатуры. Добавьте tabindex='0', чтобы сделать их фокусируемыми, реализуйте обработчики событий клавиатуры для Enter, Space и клавиш-стрелок, и убедитесь, что у них есть правильные ARIA-атрибуты. Всегда рассматривайте использование семантических HTML-элементов в первую очередь, поскольку они обеспечивают доступность клавиатуры по умолчанию.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.