Создание собственного индикатора

Библиотека Locomotive Scroll переопределяет стандартное поведение прокрутки браузера, создавая виртуальный скролл-контейнер. Это означает, что нативный scrollTop больше не является источником истины — позиция прокрутки хранится внутри экземпляра скролла.

Индикатор прокрутки (scroll indicator) должен синхронизироваться именно с этим виртуальным состоянием, а не с window.scrollY.

Ключевые данные, необходимые для построения индикатора:

  • текущая позиция прокрутки: scroll.instance.scroll.y
  • максимальная прокрутка: scroll.instance.limit.y
  • прогресс прокрутки: отношение текущей позиции к максимальной

Получение данных прокрутки

Locomotive Scroll предоставляет событие scroll, которое вызывается при каждом обновлении позиции.

const scroll = new LocomotiveScroll({
  el: document.querySelector('[data-scroll-container]'),
  smooth: true
});

scroll.on('scroll', (instance) => {
  const current = instance.scroll.y;
  const limit = instance.limit.y;
});

Здесь:

  • instance.scroll.y — текущая позиция
  • instance.limit.y — максимальная длина прокрутки

Вычисление прогресса

Процент прокрутки вычисляется простой формулой:

const progress = current / limit;

Для перевода в проценты:

const percent = progress * 100;

Это значение используется для визуального отображения состояния прокрутки.


Базовая структура HTML индикатора

Минимальная разметка:

<div class="scroll-indicator">
  <div class="scroll-indicator__progress"></div>
</div>

Стилизация индикатора

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

.scroll-indicator {
  position: fixed;
  top: 0;
  right: 20px;
  width: 4px;
  height: 100vh;
  background: rgba(0, 0, 0, 0.1);
}

.scroll-indicator__progress {
  width: 100%;
  height: 0%;
  background: #000;
  transition: height 0.1s linear;
}

Связывание данных со стилями

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

const progressBar = document.querySelector('.scroll-indicator__progress');

scroll.on('scroll', (instance) => {
  const progress = instance.scroll.y / instance.limit.y;
  progressBar.style.height = `${progress * 100}%`;
});

Горизонтальный индикатор

Аналогичная реализация, но по ширине:

.scroll-indicator {
  position: fixed;
  bottom: 0;
  left: 0;
  height: 4px;
  width: 100%;
}

.scroll-indicator__progress {
  height: 100%;
  width: 0%;
}
progressBar.style.width = `${progress * 100}%`;

Использование transform вместо width/height

Для повышения производительности предпочтительно использовать transform, так как он не вызывает перерасчет layout.

.scroll-indicator__progress {
  transform-origin: top;
  transform: scaleY(0);
}
progressBar.style.transform = `scaleY(${progress})`;

Для горизонтального варианта:

transform-origin: left;

Сглаживание анимации

Locomotive Scroll уже использует сглаживание, но индикатор можно дополнительно анимировать:

.scroll-indicator__progress {
  transition: transform 0.08s ease-out;
}

Индикатор с задержкой (lag effect)

Создание эффекта отставания индикатора:

let displayedProgress = 0;

scroll.on('scroll', (instance) => {
  const target = instance.scroll.y / instance.limit.y;

  displayedProgress += (target - displayedProgress) * 0.1;

  progressBar.style.transform = `scaleY(${displayedProgress})`;
});

Этот подход создает более плавную, “инерционную” анимацию.


Индикатор с текстовым отображением процентов

HTML:

<div class="scroll-indicator">
  <div class="scroll-indicator__progress"></div>
  <span class="scroll-indicator__text">0%</span>
</div>

Jav * aScript:

const text = document.querySelector('.scroll-indicator__text');

scroll.on('scroll', (instance) => {
  const progress = instance.scroll.y / instance.limit.y;
  const percent = Math.round(progress * 100);

  text.textContent = `${percent}%`;
});

Индикатор прогресса секции

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

Для этого используется отслеживание элементов через data-scroll и data-scroll-id.

HTML:

<section data-scroll data-scroll-id="section1">
</section>

Jav * aScript:

scroll.on('scroll', (instance) => {
  const section = instance.currentElements['section1'];

  if (section) {
    const progress = section.progress;
    progressBar.style.transform = `scaleY(${progress})`;
  }
});

Здесь progress — значение от 0 до 1 внутри элемента.


Круговой индикатор (SVG)

HTML:

<svg class="progress-ring" width="60" height="60">
  <circle
    class="progress-ring__circle"
    stroke="black"
    stroke-width="4"
    fill="transparent"
    r="26"
    cx="30"
    cy="30"
  />
</svg>

Jav * aScript:

const circle = document.querySelector('.progress-ring__circle');
const radius = circle.r.baseVal.value;
const circumference = 2 * Math.PI * radius;

circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = circumference;

scroll.on('scroll', (instance) => {
  const progress = instance.scroll.y / instance.limit.y;
  const offset = circumference - progress * circumference;

  circle.style.strokeDashoffset = offset;
});

Инвертированный индикатор

Для обратного направления:

const inverted = 1 - progress;
progressBar.style.transform = `scaleY(${inverted})`;

Работа с resize

При изменении размера окна лимит прокрутки меняется. Требуется обновление:

window.addEventListener('resize', () => {
  scroll.update();
});

Скрытие индикатора в начале или конце

if (progress <= 0.01 || progress >= 0.99) {
  progressBar.style.opacity = 0;
} else {
  progressBar.style.opacity = 1;
}

Интеграция с другими анимациями

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

  • изменение цвета страницы
  • запуск анимаций
  • параллакс-эффекты

Пример:

document.body.style.backgroundColor = `rgba(0,0,0,${progress})`;

Производительность

Ключевые рекомендации:

  • использовать transform, а не width/height
  • избегать тяжелых DOM-операций внутри scroll.on
  • минимизировать количество пересчетов
  • не использовать console.log в обработчике

Архитектурный подход

Выделение логики индикатора в отдельный модуль:

class ScrollIndicator {
  constructor(scroll, element) {
    this.scroll = scroll;
    this.el = element;
    this.progress = 0;

    this.init();
  }

  init() {
    this.scroll.on('scroll', this.update.bind(this));
  }

  update(instance) {
    const target = instance.scroll.y / instance.limit.y;
    this.progress += (target - this.progress) * 0.1;

    this.el.style.transform = `scaleY(${this.progress})`;
  }
}

Использование:

new ScrollIndicator(scroll, document.querySelector('.scroll-indicator__progress'));

Расширенные идеи

  • индикатор с градиентом, изменяющимся по мере прокрутки
  • индикатор с делениями по секциям
  • мини-карта страницы (scroll map)
  • комбинированный индикатор (линия + цифры + SVG)

Такие решения позволяют не только отображать позицию, но и усиливать восприятие структуры страницы.