Функция inView

Функция inView в Motion One предназначена для отслеживания момента, когда DOM-элемент входит или выходит из области видимости viewport. Механизм основан на IntersectionObserver, что обеспечивает высокую производительность без необходимости ручного отслеживания скролла.

Основная ценность inView заключается в декларативном подходе к запуску анимаций, ленивой инициализации эффектов и управлении состоянием элементов в зависимости от их видимости.

Базовый принцип работы

inView принимает элемент и callback-функцию, которая вызывается при изменении его пересечения с viewport. Дополнительно предоставляется объект options, аналогичный конфигурации IntersectionObserver.

Поведение можно представить как реакцию на два ключевых события:

  • элемент вошёл в область видимости
  • элемент покинул область видимости

Сигнатура функции

inView(element, (info) => {
  // логика при изменении видимости
}, options)

Параметр info содержит метаданные наблюдения:

  • entry — объект IntersectionObserverEntry
  • target — DOM-элемент
  • isVisible — логическое значение текущей видимости

Базовое использование

import { inView } from "motion";

inView(".box", ({ target }) => {
  target.style.opacity = "1";
  target.style.transform = "translateY(0px)";
});

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

Использование с анимацией

inView часто комбинируется с функцией animate, позволяя запускать анимации строго в момент появления элемента.

import { inView, animate } from "motion";

inView(".card", ({ target }) => {
  animate(
    target,
    { opacity: [0, 1], transform: ["translateY(20px)", "translateY(0px)"] },
    { duration: 0.6 }
  );
});

Здесь анимация запускается однократно при первом попадании элемента в область видимости.

Опции конфигурации

inView поддерживает настройки, влияющие на момент срабатывания:

inView(element, callback, {
  root: null,
  margin: "0px",
  amount: 0.5
});

root

Определяет контейнер пересечения. По умолчанию используется viewport.

  • null — браузерное окно
  • DOM-элемент — кастомный скролл-контейнер

margin

Позволяет расширять или сужать область срабатывания за счёт отступов.

margin: "-100px 0px -100px 0px"

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

amount

Определяет долю видимого элемента, необходимую для активации:

  • 0 — достаточно появления 1 пикселя
  • 1 — элемент должен быть полностью видим
  • промежуточные значения — процент пересечения

Однократное срабатывание

Часто требуется запуск анимации только один раз. Это достигается через возврат функции очистки или внутреннюю логику:

inView(".item", ({ target, stop }) => {
  animate(target, { opacity: [0, 1] });

  stop();
});

После вызова stop() наблюдение прекращается, предотвращая повторные срабатывания.

Управление входом и выходом

Callback может реагировать на оба состояния элемента.

inView(".panel", ({ isVisible, target }) => {
  if (isVisible) {
    target.classList.add("active");
  } else {
    target.classList.remove("active");
  }
});

Такой подход используется для синхронизации UI с прокруткой: закрепление элементов, изменение навигации, динамическая подгрузка данных.

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

inView поддерживает селекторы, автоматически применяя наблюдение ко всем совпадениям.

inView(".feature", ({ target }) => {
  animate(target, { opacity: [0, 1], y: [30, 0] });
});

Каждый элемент обрабатывается независимо, с собственным observer-ом.

Производительность и внутренний механизм

Основой работы является IntersectionObserver, который:

  • выполняется вне основного потока анимации
  • не требует постоянного polling’а scroll-событий
  • минимизирует перерасход CPU при большом количестве элементов

При этом библиотека оптимизирует подписки, объединяя наблюдение там, где это возможно, снижая нагрузку при большом количестве узлов.

Типовые сценарии применения

Lazy-reveal интерфейсов

Элементы интерфейса появляются по мере прокрутки:

inView(".section", ({ target }) => {
  animate(target, { opacity: [0, 1], scale: [0.95, 1] });
});

Активация счетчиков

inView(".counter", ({ target }) => {
  animate(0, 100, {
    duration: 2,
    onUpdate: (latest) => {
      target.textContent = Math.round(latest);
    }
  });
});

Подгрузка данных

inView(".load-trigger", async ({ target }) => {
  const data = await fetch("/api/data");
  target.innerHTML = await data.text();
});

Комбинация с CSS-состояниями

Часто inView используется без прямых анимаций, только для переключения классов:

inView(".nav", ({ isVisible, target }) => {
  target.classList.toggle("sticky", !isVisible);
});

Такой подход позволяет делегировать визуальную часть CSS, оставляя JavaScript только для логики наблюдения.

Повторные наблюдения и динамические DOM-узлы

При изменении DOM структура наблюдение можно пересоздавать или применять к новым элементам. Это важно в SPA и при виртуальном рендеринге списков.

function observeItems() {
  inView(".item", ({ target }) => {
    animate(target, { opacity: [0, 1] });
  });
}

observeItems();

Взаимодействие с scroll-контейнерами

При использовании кастомного скролла важно явно указывать root:

inView(".card", ({ target }) => {
  animate(target, { x: [50, 0] });
}, {
  root: document.querySelector(".scroll-container")
});

Это позволяет корректно отслеживать элементы внутри вложенных прокручиваемых областей.

Тонкая настройка триггеров

Комбинация margin и amount используется для создания сложных сценариев активации:

  • ранняя активация (preload-анимации)
  • поздняя активация (акцентные эффекты)
  • частичная активация (progressive reveal)
inView(".image", ({ target }) => {
  animate(target, { filter: ["blur(10px)", "blur(0px)"] });
}, {
  margin: "-20% 0px",
  amount: 0.3
});

Особенности жизненного цикла

Каждый вызов inView создаёт наблюдатель, который существует до:

  • удаления элемента из DOM
  • вызова функции остановки
  • разрушения контекста наблюдения

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