Плавная прокрутка к элементу

Прокрутка страницы представляет собой изменение значения скролла документа или контейнера по вертикали или горизонтали. В стандартном DOM API поведение контролируется через window.scrollTo, element.scrollIntoView и свойство scroll-behavior: smooth, которое задаёт встроенную анимацию перемещения.

html {
  scroll-behavior: smooth;
}

Такой подход ограничен отсутствием контроля над кривыми анимации, временем и синхронизацией с другими эффектами интерфейса. Библиотека Motion One добавляет возможность управлять скроллом как анимацией: задавать easing, длительность, триггеры и связывать прокрутку с состоянием интерфейса.


Прокрутка окна к координате

Базовый сценарий — программная прокрутка страницы к конкретной позиции.

В нативном варианте:

window.scrollTo({
  top: 800,
  beh * avior: "smooth"
});

При использовании Motion One логика расширяется через анимацию числового значения прокрутки. Концептуально прокрутка рассматривается как изменяемая величина scrollY.

import { animate } from "motion";

animate(window.scrollY, 800, {
  duration: 0.8,
  ease: "easeInOut",
  onUpdate: (latest) => {
    window.scrollTo(0, latest);
  }
});

Такой подход позволяет полностью контролировать траекторию движения, а не полагаться на браузерную реализацию smooth scroll.


Прокрутка к DOM-элементу

Основной сценарий интерфейсов — переход к конкретному элементу по клику или событию.

Базовая реализация через offsetTop

const target = document.querySelector("#section-3");

window.scrollTo({
  top: target.offsetTop,
  beh * avior: "smooth"
});

Однако offsetTop не учитывает сложные layout-сценарии (flex, nested scroll containers, sticky элементы).

Более точный расчёт через getBoundingClientRect

const target = document.querySelector("#section-3");

const top =
  target.getBoundingClientRect().top +
  window.scrollY;

window.scrollTo({
  top,
  beh * avior: "smooth"
});

Реализация через Motion One

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

import { animate } from "motion";

function scrollToElement(el) {
  const targetY =
    el.getBoundingClientRect().top +
    window.scrollY;

  animate(window.scrollY, targetY, {
    duration: 0.9,
    ease: "easeInOut",
    onUpdate: (latest) => {
      window.scrollTo(0, latest);
    }
  });
}

document.querySelector("button")
  .addEventListener("click", () => {
    scrollToElement(document.querySelector("#section-3"));
  });

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


Компенсация фиксированного хедера

Частая проблема — перекрытие целевого элемента фиксированной панелью навигации.

Базовая корректировка

const headerHeight = 80;

const top =
  el.getBoundingClientRect().top +
  window.scrollY -
  headerHeight;

window.scrollTo({
  top,
  beh * avior: "smooth"
});

Адаптивная компенсация

При изменяемой высоте хедера:

function getHeaderHeight() {
  const header = document.querySelector("header");
  return header ? header.offsetHeight : 0;
}
const top =
  el.getBoundingClientRect().top +
  window.scrollY -
  getHeaderHeight();

Прокрутка внутри контейнера

Не все интерфейсы используют window как основной scroll context. Часто прокрутка реализуется внутри блока с overflow: auto.

HTML структура

<div class="scroll-container">
  <div class="item" id="target"></div>
</div>
.scroll-container {
  height: 400px;
  overflow-y: auto;
}

Нативный способ

const container = document.querySelector(".scroll-container");
const target = document.querySelector("#target");

container.scrollTo({
  top: target.offsetTop,
  beh * avior: "smooth"
});

Точный расчёт относительно контейнера

const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();

const top = targetRect.top - containerRect.top + container.scrollTop;

container.scrollTo({
  top,
  beh * avior: "smooth"
});

Motion One внутри контейнера

import { animate } from "motion";

function scrollContainerTo(container, target) {
  const top =
    target.getBoundingClientRect().top -
    container.getBoundingClientRect().top +
    container.scrollTop;

  animate(container.scrollTop, top, {
    duration: 0.8,
    ease: "easeOut",
    onUpdate: (v) => {
      container.scrollTop = v;
    }
  });
}

Использование animate() для управления скроллом

Motion One рассматривает анимацию как интерполяцию значений. Прокрутка — это частный случай изменения числового состояния.

Управление скоростью и easing-кривыми

animate(0, 1200, {
  duration: 1.2,
  ease: [0.22, 1, 0.36, 1],
  onUpdate: (value) => {
    window.scrollTo(0, value);
  }
});

Cubic bezier-кривые позволяют добиться более естественного движения, близкого к инерции интерфейсов мобильных систем.


Связь прокрутки и состояния интерфейса

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

Отслеживание положения скролла

window.addEventListener("scroll", () => {
  const scrollY = window.scrollY;

  document.body.style.setProperty(
    "--scroll",
    scrollY
  );
});

Далее значение используется в CSS-анимациях.


Синхронизация с анимацией появления элементов

При прокрутке к элементу часто требуется не только переместить viewport, но и запустить анимацию появления.

import { animate, inView } from "motion";

const el = document.querySelector("#section-3");

inView(el, () => {
  animate(el, { opacity: 1, y: 0 }, {
    duration: 0.6,
    ease: "easeOut"
  });
});

Комбинация scrollTo и inView позволяет строить сцены интерфейса, где прокрутка выступает триггером анимационных последовательностей.


Прокрутка с задержкой анимации элементов

Сценарий: сначала прокрутка, затем каскадное появление контента.

function scrollThenAnimate(el) {
  const top =
    el.getBoundingClientRect().top +
    window.scrollY;

  animate(window.scrollY, top, {
    duration: 0.8,
    ease: "easeInOut",
    onUpdate: (v) => window.scrollTo(0, v),
    onComplete: () => {
      animate(el.querySelectorAll(".child"), {
        opacity: [0, 1],
        y: [20, 0]
      }, {
        delay: (i) => i * 0.05,
        duration: 0.5
      });
    }
  });
}

Прокрутка с учётом динамического контента

При загрузке данных высота страницы может изменяться, что влияет на целевые координаты.

async function scrollAfterLoad(el, loadFn) {
  await loadFn();

  requestAnimationFrame(() => {
    const top =
      el.getBoundingClientRect().top +
      window.scrollY;

    animate(window.scrollY, top, {
      duration: 0.9,
      ease: "easeInOut",
      onUpdate: (v) => window.scrollTo(0, v)
    });
  });
}

Прерывание и повторная прокрутка

При частых вызовах прокрутки важно предотвращать конфликт анимаций.

let currentAnimation;

function smoothScrollTo(y) {
  if (currentAnimation) currentAnimation.cancel();

  currentAnimation = animate(window.scrollY, y, {
    duration: 0.7,
    ease: "easeOut",
    onUpdate: (v) => window.scrollTo(0, v)
  });
}

Такой подход предотвращает дергание интерфейса при множественных событиях навигации.


Использование относительной прокрутки

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

function scrollByOffset(offset) {
  const start = window.scrollY;
  const end = start + offset;

  animate(start, end, {
    duration: 0.6,
    ease: "easeInOut",
    onUpdate: (v) => window.scrollTo(0, v)
  });
}

Параллельная анимация прокрутки и UI-элементов

Motion One позволяет синхронизировать движение страницы с интерфейсными эффектами.

function scrollWithHeaderEffect(el) {
  const top =
    el.getBoundingClientRect().top +
    window.scrollY;

  animate(window.scrollY, top, {
    duration: 1,
    ease: "easeInOut",
    onUpdate: (v) => {
      window.scrollTo(0, v);

      document.querySelector("header")
        .style.transform =
        `translateY(${v * 0.02}px)`;
    }
  });
}

Декларативная логика прокрутки в архитектуре интерфейса

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