Клавиатурная навигация

Клавиатурная навигация в интерфейсах с анимациями Lottie становится критическим аспектом доступности, когда анимация перестаёт быть декоративной и начинает взаимодействовать с пользователем. В таких случаях важно обеспечить полный контроль с клавиатуры над воспроизведением, паузой, перемоткой и переходом по кадрам, сохраняя предсказуемое поведение и корректную интеграцию с API Lottie Web.

Библиотека Lottie Web предоставляет объект анимации, через который осуществляется управление воспроизведением:

  • play() — запуск анимации
  • pause() — остановка
  • stop() — сброс в начало
  • goToAndStop(frame, isFrame) — переход к конкретному кадру
  • goToAndPlay(frame, isFrame) — переход и запуск
  • setSpeed(speed) — изменение скорости
  • destroy() — уничтожение экземпляра

Клавиатурная навигация строится поверх этих методов, превращая их в реакцию на события клавиатуры.

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

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

<div id="lottie" tabindex="0" aria-label="Анимация" role="application"></div>

Ключевые моменты:

  • tabindex="0" делает элемент доступным через Tab
  • aria-label задаёт смысловое описание
  • role="application" используется, когда внутри есть сложное интерактивное поведение

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

aria-hidden="true"

и клавиатурное управление не подключается.

Инициализация Lottie Web

import lottie from "lottie-web";

const animation = lottie.loadAnimation({
  container: document.getElementById("lottie"),
  renderer: "svg",
  loop: false,
  autoplay: false,
  path: "/animation.json"
});

Для клавиатурной навигации важно отключить autoplay, чтобы управление полностью принадлежало пользователю.

Слой обработки клавиатурных событий

Основная логика строится вокруг keydown. Используется именно это событие, чтобы перехватывать действие до стандартной обработки браузера.

const container = document.getElementById("lottie");

container.addEventListener("keydown", (e) => {
  switch (e.key) {
    case " ":
    case "Spacebar":
      e.preventDefault();
      togglePlay();
      break;

    case "ArrowRight":
      e.preventDefault();
      stepForward();
      break;

    case "ArrowLeft":
      e.preventDefault();
      stepBackward();
      break;

    case "Home":
      e.preventDefault();
      animation.goToAndStop(0, true);
      break;

    case "End":
      e.preventDefault();
      animation.goToAndStop(animation.totalFrames, true);
      break;
  }
});

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

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

Функция переключения состояния:

let isPlaying = false;

function togglePlay() {
  if (isPlaying) {
    animation.pause();
  } else {
    animation.play();
  }
  isPlaying = !isPlaying;
}

Состояние лучше отслеживать явно, так как Lottie Web не всегда предоставляет удобный реактивный флаг воспроизведения.

Пошаговая навигация по кадрам

Для точной клавиатурной навигации используется управление кадрами:

function stepForward() {
  const frame = animation.currentFrame + 1;
  animation.goToAndStop(frame, true);
}

function stepBackward() {
  const frame = animation.currentFrame - 1;
  animation.goToAndStop(frame, true);
}

Важный момент — использование goToAndStop, а не seek, поскольку Lottie Web работает через явные переходы кадров.

Ускоренная навигация

Для более удобного взаимодействия можно вводить ускоренные шаги:

  • Shift + стрелки — перемотка по 10 кадров
  • Ctrl + стрелки — переход по 1 секунде (если известен framerate)
function stepForwardLarge() {
  animation.goToAndStop(animation.currentFrame + 10, true);
}

Если известен FPS:

const FPS = 60;

function stepForwardSecond() {
  animation.goToAndStop(animation.currentFrame + FPS, true);
}

Обработка удержания клавиш

Для плавной перемотки может использоваться удержание клавиши:

let interval;

container.addEventListener("keydown", (e) => {
  if (e.key === "ArrowRight" && !interval) {
    interval = setInterval(stepForward, 50);
  }
});

container.addEventListener("keyup", (e) => {
  if (e.key === "ArrowRight") {
    clearInterval(interval);
    interval = null;
  }
});

Это создаёт эффект “скраббинга” по анимации, но требует осторожности, чтобы не перегружать main thread.

Синхронизация состояния и доступности

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

function updateAriaState() {
  container.setAttribute(
    "aria-busy",
    animation.isPaused ? "false" : "true"
  );
}

Также можно добавлять:

  • aria-valuemin
  • aria-valuemax
  • aria-valuenow

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

Реализация роли слайдера

Если анимация управляется как временная шкала, контейнер можно представить как ползунок:

<div
  id="lottie"
  role="slider"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuenow="0"
  tabindex="0">
</div>

Обновление значения:

function updateProgress() {
  const progress = (animation.currentFrame / animation.totalFrames) * 100;

  container.setAttribute("aria-valuenow", Math.round(progress));
}

Интеграция с requestAnimationFrame

Для синхронизации UI и анимации используется цикл обновления:

function loop() {
  updateProgress();
  requestAnimationFrame(loop);
}

loop();

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

Обработка фокуса и blur

Клавиатурная навигация должна активироваться только при фокусе:

container.addEventListener("focus", () => {
  container.classList.add("focused");
});

container.addEventListener("blur", () => {
  container.classList.remove("focused");
});

И при необходимости можно приостанавливать анимацию:

container.addEventListener("blur", () => {
  animation.pause();
  isPlaying = false;
});

Предотвращение конфликтов с браузером

Некоторые клавиши имеют системное поведение (например, Space прокручивает страницу). Поэтому обязательна явная блокировка:

  • preventDefault() для Space
  • контроль стрелок при активном фокусе
  • избегание глобальных обработчиков без проверки target
if (document.activeElement !== container) return;

Поддержка нескольких анимаций

При наличии нескольких Lottie-инстансов клавиатурное управление должно быть изолировано:

document.querySelectorAll(".lottie").forEach((el) => {
  el.addEventListener("keydown", handler);
});

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

let activeAnimation = null;

container.addEventListener("focus", () => {
  activeAnimation = animation;
});

Паттерн централизованного контроллера

Для сложных интерфейсов применяется единый контроллер:

class LottieKeyboardController {
  constructor(animation, container) {
    this.animation = animation;
    this.container = container;
    this.bind();
  }

  bind() {
    this.container.addEventListener("keydown", this.onKeyDown.bind(this));
  }

  onKeyDown(e) {
    if (e.key === " ") {
      e.preventDefault();
      this.toggle();
    }
  }

  toggle() {
    this.animation.isPaused
      ? this.animation.play()
      : this.animation.pause();
  }
}

Особенности SVG-рендерера

При использовании renderer: "svg" клавиатурная навигация может взаимодействовать с DOM-элементами внутри SVG. Это требует:

  • отключения pointer-events на декоративных слоях
  • предотвращения перехвата фокуса SVG-элементами
  • контроля tabindex внутри SVG (если экспорт содержит интерактивные слои)

Производительность при частых обновлениях

Частая перемотка клавишами может приводить к нагрузке:

  • избегание лишних goToAndStop вызовов
  • дебаунс при удержании клавиш
  • ограничение FPS обновлений UI
let lastUpdate = 0;

function throttledUpdate() {
  const now = performance.now();
  if (now - lastUpdate > 16) {
    updateProgress();
    lastUpdate = now;
  }
}

Расширение модели управления

Поверх базовой клавиатурной навигации можно добавлять:

  • управление скоростью (+ / -)
  • переключение цикличности (L)
  • мгновенный сброс (R)
  • переключение режимов рендеринга

Каждое действие должно быть явно документировано через aria-keyshortcuts:

<div aria-keyshortcuts="Space ArrowLeft ArrowRight Home End"></div>