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

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

Визуальный индикатор фокуса сообщает, какой элемент в данный момент активен для взаимодействия. Без такого индикатора пользователь клавиатуры теряет ориентацию внутри страницы. Это особенно критично для людей:

  • использующих клавиатуру вместо мыши;
  • работающих со вспомогательными технологиями;
  • имеющих нарушения моторики;
  • использующих экранные лупы.

По стандартам доступности (WCAG) наличие видимого индикатора фокуса является обязательным требованием.

Во многих браузерах индикатор реализован по умолчанию — чаще всего это контур (outline). Однако стандартное оформление не всегда соответствует визуальному стилю интерфейса, поэтому разработчики часто заменяют его собственными решениями. При неправильной реализации это может привести к полной потере индикатора.

Библиотека ally.js предоставляет инструменты для управления поведением фокуса, включая безопасную кастомизацию его визуального отображения.


Поведение стандартного индикатора фокуса

По умолчанию браузеры применяют CSS-правило:

:focus {
  outline: auto;
}

В зависимости от браузера отображается:

  • синяя рамка
  • пунктирный контур
  • светящийся ореол

Пример стандартного поведения:

<button>Сохранить</button>

При переходе к кнопке с клавиатуры появляется рамка.

Распространённая ошибка — полное отключение индикатора:

:focus {
  outline: none;
}

Такое правило полностью удаляет визуальный сигнал фокуса. Пользователь клавиатуры больше не понимает, где находится курсор взаимодействия.

При кастомизации требуется соблюдать два принципа:

  1. Индикатор должен оставаться заметным.
  2. Он должен отображаться только тогда, когда это необходимо.

Для решения второй задачи ally.js предлагает механизм определения способа взаимодействия пользователя.


Проблема различия между мышью и клавиатурой

Пользователь, работающий мышью, не нуждается в постоянном отображении индикаторов фокуса. Например:

  • при клике на кнопку рамка может выглядеть визуально лишней;
  • при работе с формой она может конфликтовать с дизайном.

В современных интерфейсах принято:

  • показывать индикатор при навигации с клавиатуры
  • скрывать его при кликах мышью

Нативный CSS-псевдокласс :focus не умеет различать источник взаимодействия. Для решения этой задачи в браузерах появился псевдокласс :focus-visible, но его поддержка появилась относительно недавно.

Библиотека ally.js реализует подобную логику независимо от поддержки браузера.


Определение источника взаимодействия

В ally.js существует модуль what-input и связанные механизмы определения того, каким способом пользователь взаимодействует со страницей.

Основная идея:

  • при нажатии клавиши фиксируется keyboard input
  • при клике мыши фиксируется mouse input
  • при касании экрана фиксируется touch input

На основании этих данных можно управлять отображением индикатора фокуса.

Пример логики:

let usingKeyboard = false;

document.addEventListener('keydown', () => {
  usingKeyboard = true;
});

document.addEventListener('mousedown', () => {
  usingKeyboard = false;
});

Эта примитивная реализация показывает принцип, который ally.js реализует более корректно и устойчиво.


Использование ally.style.focusWithin

Один из инструментов библиотеки — управление стилями элементов, находящихся в состоянии фокуса или содержащих его внутри.

Модуль позволяет применять стили не только к самому элементу, но и к его контейнеру.

Пример:

import focusWithin from 'ally.js/style/focus-within';

const handle = focusWithin({
  context: document.body
});

После активации модуля контейнер получает CSS-класс:

ally-focus-within

Этот класс можно использовать для стилизации интерфейса.

Пример CSS:

.ally-focus-within button:focus {
  outline: 3px solid #005fcc;
}

Такой подход помогает создавать групповые индикаторы фокуса.


Кастомный индикатор фокуса

Индикатор может быть реализован различными способами:

  • рамка (outline)
  • тень (box-shadow)
  • изменение фона
  • подсветка контейнера
  • псевдоэлемент

Наиболее гибким считается использование box-shadow.

Пример

button:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.6);
}

Преимущества:

  • не влияет на размеры элемента
  • хорошо масштабируется
  • легко анимируется

Контекстные индикаторы фокуса

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

Пример: поле ввода внутри формы.

HTML:

<div class="form-field">
  <label>Email</label>
  <input type="email">
</div>

CSS:

.form-field.focused {
  border-color: #2a7cff;
}

JavaScript с ally.js:

import observeFocus from 'ally.js/observe/focus';

observeFocus({
  context: document.body,
  callback(element) {
    document
      .querySelectorAll('.form-field')
      .forEach(el => el.classList.remove('focused'));

    const field = element.closest('.form-field');

    if (field) {
      field.classList.add('focused');
    }
  }
});

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


Индикаторы фокуса для составных компонентов

Современные интерфейсы часто используют кастомные компоненты:

  • выпадающие списки
  • меню
  • табы
  • модальные окна
  • карусели

Фокус может находиться внутри сложной структуры, поэтому важно корректно отображать активный элемент.

Пример компонента меню:

<ul class="menu">
  <li><button>Файл</button></li>
  <li><button>Правка</button></li>
  <li><button>Вид</button></li>
</ul>

Кастомный индикатор:

.menu button:focus {
  outline: none;
  background: #e5f1ff;
}

С ally.js можно отслеживать текущий элемент и менять состояние родителя.


Управление фокусом через observe

Модуль ally.js/observe/focus отслеживает изменения активного элемента.

Пример:

import observeFocus from 'ally.js/observe/focus';

const observer = observeFocus({
  context: document.body,
  callback: function(element) {
    console.log('Focus moved to', element);
  }
});

Это позволяет:

  • создавать динамические индикаторы
  • синхронизировать фокус с интерфейсом
  • добавлять анимации

Анимированные индикаторы

Кастомный индикатор может быть анимированным, что улучшает визуальную ориентацию.

Пример:

button {
  transition: box-shadow 0.2s ease;
}

button:focus {
  outline: none;
  box-shadow: 0 0 0 4px rgba(80,150,255,0.5);
}

Эффект плавного появления облегчает восприятие перемещения фокуса.


Индикаторы для клавиатурной навигации

Часто используется паттерн:

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

CSS:

.focus-visible {
  box-shadow: 0 0 0 3px #2a7cff;
}

JavaScript с ally.js:

document.addEventListener('keydown', () => {
  document.body.classList.add('keyboard-navigation');
});

document.addEventListener('mousedown', () => {
  document.body.classList.remove('keyboard-navigation');
});

CSS:

.keyboard-navigation :focus {
  outline: 3px solid #2a7cff;
}

Индикатор появляется только при клавиатурной навигации.


Индикаторы внутри модальных окон

Модальные окна требуют особого контроля фокуса:

  • фокус должен оставаться внутри окна
  • индикаторы должны быть четко видны

При использовании ally.js можно сочетать focus trap и кастомный индикатор.

CSS:

.modal :focus {
  outline: none;
  box-shadow: 0 0 0 3px #ff9800;
}

Фокус становится хорошо заметным внутри диалога.


Улучшение контрастности индикаторов

Индикатор должен соответствовать требованиям контрастности.

Рекомендуемые параметры:

  • толщина не менее 2–3 px
  • контраст не ниже 3:1
  • хорошо заметен на любом фоне

Пример универсального индикатора:

:focus {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

outline-offset помогает отделить рамку от элемента.


Использование псевдоэлементов

Иногда индикатор создается с помощью ::after.

Пример:

button {
  position: relative;
}

button:focus::after {
  content: "";
  position: absolute;
  inset: -4px;
  border: 2px solid #2a7cff;
}

Это дает полный контроль над внешним видом индикатора.


Типичные ошибки кастомизации

Полное удаление outline

:focus {
  outline: none;
}

Без альтернативного индикатора это делает интерфейс недоступным.


Слишком слабый контраст

:focus {
  outline: 1px solid #ccc;
}

Такой индикатор практически не виден.


Индикатор только при hover

Некоторые интерфейсы показывают подсветку только при наведении мыши, но не при фокусе клавиатурой.


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

Иногда рамка скрывается из-за overflow: hidden.

Решение:

  • использовать box-shadow
  • увеличивать outline-offset

Практический пример комплексной кастомизации

HTML:

<button class="primary">Отправить</button>

CSS:

.primary {
  background: #2a7cff;
  color: white;
  border: none;
  padding: 10px 16px;
}

.primary:focus {
  outline: none;
  box-shadow:
    0 0 0 3px white,
    0 0 0 6px #2a7cff;
}

Результат:

  • двойной индикатор
  • хорошо заметен на любом фоне
  • соответствует современным дизайн-системам.

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

Библиотека ally.js предоставляет инструменты:

  • отслеживание активного элемента
  • управление фокусом
  • ограничение области фокуса
  • обнаружение интерактивных элементов

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

  • focus traps
  • roving tabindex
  • динамических интерфейсов

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