Back

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

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

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

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

  • Структурируйте 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, выявите проблемы управления фокусом и реализуйте паттерны, описанные в этом руководстве. Ваши пользователи будут благодарны за создание более инклюзивного опыта.

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

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

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

Реструктурируйте ваш 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