Как создать эффект липкой прокрутки с помощью JavaScript

Как создать эффект липкой прокрутки с помощью JavaScript

Содержание
  1. Live Demo / Download.
  2. Создание HTML-структуры с разделами
  3. Начало работы с JavaScript: Установка высоты контейнера
  4. Определение точек прокрутки между секциями
  5. Определение индекса отображаемой секции
  6. Управление видимостью секций и эффектами входа/выхода
  7. Выводы

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

Live Demo / Download.

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

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

Создание HTML-структуры с разделами

Для этого примера мы использовали Tailwind CSS. Поскольку основное внимание здесь уделено JavaScript, я не буду объяснять CSS-часть. Вы можете просто использовать этот готовый HTML.

<div class="mx-auto max-w-md lg:max-w-none">
	<div class="space-y-16 lg:sticky lg:top-0 lg:min-h-screen lg:space-y-0">
		<!-- Секция #1 -->
		<section class="lg:absolute lg:inset-0">
			<div
				class="flex flex-col space-y-4 space-y-reverse lg:min-h-full lg:flex-row lg:space-x-20 lg:space-y-0"
			>
				<div class="order-1 flex flex-1 items-center lg:order-none">
					<div class="space-y-3">
						<div class="relative inline-flex font-semibold text-indigo-500">
							Интегрированные знания
							<svg
								class="absolute top-full w-full fill-indigo-300"
								xmlns="http://www.w3.org/2000/svg"
								width="166"
								height="4"
							>
								<path
									d="M98.865 1.961c-8.893.024-17.475.085-25.716.182-2.812.019-5.023.083-7.622.116l-6.554.067a2910.9 2910.9 0 0 0-25.989.38c-4.04.067-7.709.167-11.292.27l-1.34.038c-2.587.073-4.924.168-7.762.22-2.838.051-6.054.079-9.363.095-1.994.007-2.91-.08-3.106-.225l-.028-.028c-.325-.253.203-.463 1.559-.62l.618-.059c.206-.02.42-.038.665-.054l1.502-.089 3.257-.17 2.677-.132c.902-.043 1.814-.085 2.744-.126l1.408-.06c4.688-.205 10.095-.353 16.167-.444C37.413 1.22 42.753.98 49.12.824l1.614-.037C54.041.707 57.588.647 61.27.6l1.586-.02c4.25-.051 8.53-.1 12.872-.14C80.266.4 84.912.373 89.667.354l2.866-.01c8.639-.034 17.996 0 27.322.03 6.413.006 13.168.046 20.237.12l2.368.027c1.733.014 3.653.05 5.712.105l2.068.064c5.89.191 9.025.377 11.823.64l.924.09c.802.078 1.541.156 2.21.233 1.892.233.29.343-3.235.364l-3.057.02c-.446.003-.89.008-1.33.014a305.77 305.77 0 0 1-4.33-.004c-2.917-.005-5.864-.018-8.783-.019l-4.982.003a447.91 447.91 0 0 1-3.932-.02l-4.644-.023-4.647-.014c-9.167-.026-18.341-.028-26.923.03l-.469-.043Z"
								/>
							</svg>
						</div>
						<h2 class="font-extrabold text-slate-900 text-4xl">
							Поддержите своих пользователей с помощью популярных тем
						</h2>
						<p class="text-slate-500 text-lg">
							Статистика показывает, что люди, просматривающие вашу веб-страницу и получающие живую
							помощь с помощью виджета чата, с большей вероятностью совершат покупку.
						</p>
					</div>
				</div>
				<div class="flex flex-1 items-center">
					<img width="512" height="480" src="./illustration-01.png" alt="Иллюстрация 01" />
				</div>
			</div>
		</section>
	</div>
</div>

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

Макет раздела состоит из текста слева и изображения справа. Он разработан таким образом, чтобы быть отзывчивым и отображаться на небольших экранах. Обратите внимание на минимальную высоту контейнера, равную высоте области просмотра, и использование абсолютного позиционирования для секций. Такой подход позволяет нам легко складывать секции в стопку, уделяя основное внимание их видимости и переходам.

Важно: Эффект перехода будет активен только на экранах размером более 1024px. Это решение гарантирует, что контент останется доступным на небольших экранах, где пространство может быть ограничено.

Начало работы с JavaScript: Установка высоты контейнера

Наша цель - сделать так, чтобы разделы перекрывались при прокрутке страницы вниз. Чтобы добиться этого, мы должны отслеживать, когда прокрутка достигает точки, в которой должна стать видна следующая секция. Для этого нам нужно рассчитать гипотетическую общую высоту контейнера, если бы все секции были сложены одна за другой.

JavaScript может помочь нам рассчитать это value. Предполагая, что каждая секция имеет высоту, равную высоте области просмотра (т. е. lg:h-screen), мы назначим контейнеру минимальную высоту 100vh, умноженную на количество секций плюс 1. Добавление одной единицы гарантирует, что последняя секция останется на экране, как и остальные, а не исчезнет при прокрутке.

Давайте запустим нашу настройку JavaScript:

  класс StickySections {
    constructor(containerElement) {
      this.container = {
        el: containerElement,
      }
      this.sections = Array.from(this.container.el.querySelectorAll('section'));
      this.initContainer = this.initContainer.bind(this);
      this.init();
    }

    initContainer() {
      this.container.el.style.setProperty('--stick-items', `${this.sections.length + 1}00vh`);
    }

    init() {
      this.initContainer();
    }
  }

  // Инициация StickySections
  const sectionsContainer = document.querySelectorAll('[data-sticky-sections]');
  sectionsContainer.forEach((section) => {
    new StickySections(section);
  });

Теперь мы определяем элемент контейнера с помощью атрибута data-sticky-sections. Произвольные варианты Tailwind CSS позволяют нам динамически задавать высоту контейнера:

<div class="mx-auto max-w-md lg:min-h-[var(--stick-items)] lg:max-w-none" data-sticky-sections>
	. ...
</div>

Например, при наличии 3 секций высота контейнера будет установлена на 400vh.

Определение точек прокрутки между секциями

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

Мы создадим переменную scrollValue, изначально установленную в 0. Значения для этой переменной следуют следующей логике:

  • Если верхний край контейнера находится ниже верхнего края области просмотра, scrollValue устанавливается в 0.
  • Если нижний край контейнера находится выше верхнего края области просмотра, scrollValue равняется количеству секций плюс 1 (например, при 3 секциях scrollValue равняется 4).
  • Когда контейнер пересекает верхний край области просмотра, значения попадают в определенный диапазон.

Давайте применим эту теорию на практике:

  класс StickySections {
    constructor(containerElement) {
      this.container = {
        el: containerElement,
        height: 0,
        верх: 0,
        низ: 0,
      }
      this.sections = Array.from(this.container.el.querySelectorAll('section'));
      this.viewportTop = 0;
      this.scrollValue = 0; // Значение прокрутки липкого контейнера
      this.onScroll = this.onScroll.bind(this);
      this.initContainer = this.initContainer.bind(this);
      this.handleSections = this.handleSections.bind(this);
      this.remapValue = this.remapValue.bind(this);
      this.init();
    }

    onScroll() {
      this.handleSections();
    }

    initContainer() {
      this.container.el.style.setProperty('--stick-items', `${this.sections.length + 1}00vh`);
    }

    handleSections() {
      this.viewportTop = window.scrollY;
      this.container.height = this.container.el.clientHeight;
      this.container.top = this.container.el.offsetTop;
      this.container.bottom = this.container.top + this.container.height;

      if (this.container.bottom <= this.viewportTop) {
        // Нижний край stickContainer находится выше области просмотра
        this.scrollValue = this.sections.length + 1;
      } else if (this.container.top >= this.viewportTop) {
        // Верхний край контейнера stickContainer находится ниже области просмотра
        this.scrollValue = 0;
      } else {
        // Контейнер stickContainer пересекается с областью просмотра
        this.scrollValue = this.remapValue(this.viewportTop, this.container.top, this.container.bottom, 0, this.sections.length + 1);
      }
    }

    // Эта функция переводит значение из одного диапазона в другой диапазон
    remapValue(value, start1, end1, start2, end2) {
      const remapped = (value - start1) * (end2 - start2) / (end1 - start1) + start2;
      return remapped > 0 ? remapped : 0;
    }

    init() {
      this.initContainer();
      this.handleSections();
      window.addEventListener('scroll', this.onScroll);
    }
  }

  // Инициализация StickySections
  const sectionsContainer = document.querySelectorAll('[data-sticky-sections]');
  sectionsContainer.forEach((section) => {
    new StickySections(section);
  });

Я ввел значительный объем кода, поэтому давайте разберем его пошагово:

  • Я зарегистрировал событие scroll в окне и создал функцию onScroll, которая будет вызываться каждый раз, когда происходит прокрутка.
  • Функция onScroll, в свою очередь, вызывает метод handleSections. Смысл создания отдельного метода заключается в том, чтобы избежать дублирования кодаация, поскольку handleSections также должна быть вызвана во время инициализации.
  • Я ввел дополнительный набор переменных, необходимых для вычисления значений scrollValue:
    • viewportTop представляет значение прокрутки в пикселях.
    • container.height соответствует высоте контейнера в пикселях.
    • container.top указывает расстояние в пикселях между верхним краем контейнера и верхним краем документа.
    • container.bottom обозначает расстояние в пикселях между нижним краем контейнера и верхним краем документа. Эти переменные позволят нам определить значение scrollValue, как уже объяснялось ранееd.
  • Для полноты картины в игру вступает метод remapValue, позволяющий нам переназначить значение viewportTop в заданном диапазоне значений (от 0 до 4, предполагая 3 секции).

Переменная scrollValue динамически обновляется по мере прокрутки, позволяя нам определить индекс секции для отображения.

Определение индекса отображаемой секции

Этот шаг может показаться сложным, но на самом деле он прост. Мы создаем переменную activeIndex, первоначально установленную в 0 (указывающую на то, что должен отображаться первый раздел).

Внутри метода handleSections мы обновляем activeIndex с помощью этой строки:

this.activeIndex =
	Math.floor(this.scrollValue) >= this.sections.length
		? this.sections.length - 1
		: Math.floor(this.scrollValue);

Это простой тернарный оператор, отвечающий на вопрос: “Является ли значение scrollValue больше или равно количеству секций?“. Или, если упростить, “Последняя секция пересекает или вышла вверх за верхний край области просмотра?“. Возможны два варианта ответа:

В противном случае activeIndex равен scrollValue с округлением вниз.

  • Да, и activeIndex будет равен последнему индексу секций.
  • Нет, и activeIndex будет равен значению scrollValue, округленному вниз.

Управление видимостью секций и эффектами входа/выхода

Мы подходим к концу этого руководства! Теперь, когда мы определили индекс секции, которую нужно отобразить, завершение эффекта не составит труда. Существует несколько подходов; в данном примере мы используем цикл forEach и переменные CSS:

this.sections.forEach((section, i) => {
	if (i === this.activeIndex) {
		section.style.setProperty('--stick-visibility', '1');
		section.style.setProperty('--stick-scale', '1');
	} else {
		section.style.setProperty('--stick-visibility', '0');
		section.style.setProperty('--stick-scale', '.8');
	}
});

По сути, если текущая секция должна отображаться, мы устанавливаем visibility и scale в 1. В противном случае мы устанавливаем видимость в 0 и масштаб в 0.8.

Теперь давайте применим эти CSS-переменные в HTML, используя Tailwind CSS arbitrary variants:

<section class="lg:absolute lg:inset-0 lg:z-[var(--stick-visibility)]">
	<div
		class="flex flex-col space-y-4 space-y-reverse lg:h-full lg:flex-row lg:space-x-20 lg:space-y-0"
	>
		<div
			class="order-1 flex flex-1 items-center transition-opacity duration-300 lg:order-none lg:opacity-[var(--stick-visibility)]"
		>
			<div class="space-y-3">
				<div class="relative inline-flex font-semibold text-indigo-500">
					Интегрированные знания
					<svg
						class="absolute top-full w-full fill-indigo-300"
						xmlns="http://www.w3.org/2000/svg"
						width="166"
						height="4"
					>
						<path
							d="M98.865 1.961c-8.893.024-17.475.085-25.716.182-2.812.019-5.023.083-7.622.116l-6.554.067a2910.9 2910.9 0 0 0-25.989.38c-4.04.067-7.709.167-11.292.27l-1.34.038c-2.587.073-4.924.168-7.762.22-2.838.051-6.054.079-9.363.095-1.994.007-2.91-.08-3.106-.225l-.028-.028c-.325-.253.203-.463 1.559-.62l.618-.059c.206-.02.42-.038.665-.054l1.502-.089 3.257-.17 2.677-.132c.902-.043 1.814-.085 2.744-.126l1.408-.06c4.688-.205 10.095-.353 16.167-.444C37.413 1.22 42.753.98 49.12.824l1.614-.037C54.041.707 57.588.647 61.27.6l1.586-.02c4.25-.051 8.53-.1 12.872-.14C80.266.4 84.912.373 89.667.354l2.866-.01c8.639-.034 17.996 0 27.322.03 6.413.006 13.168.046 20.237.12l2.368.027c1.733.014 3.653.05 5.712.105l2.068.064c5.89.191 9.025.377 11.823.64l.924.09c.802.078 1.541.156 2.21.233 1.892.233.29.343-3.235.364l-3.057.02c-.446.003-.89.008-1.33.014a305.77 305.77 0 0 1-4.33-.004c-2.917-.005-5.864-.018-8.783-.019l-4.982.003a447.91 447.91 0 0 1-3.932-.02l-4.644-.023-4.647-.014c-9.167-.026-18.341-.028-26.923.03l-.469-.043Z"
						/>
					</svg>
				</div>
				<h2 class="font-extrabold text-slate-900 text-4xl">
					Поддержите своих пользователей с помощью популярных тем
				</h2>
				<p class="text-slate-500 text-lg">
					Статистика показывает, что люди, просматривающие вашу веб-страницу и получающие живую
					помощь с помощью виджета чата, с большей вероятностью совершат покупку.
				</p>
			</div>
		</div>
		<div
			class="flex flex-1 items-center transition duration-300 lg:scale-[var(--stick-scale)] lg:opacity-[var(--stick-visibility)]"
		>
			<img width="512" height="480" src="./illustration-01.png" alt="Иллюстрация 01" />
		</div>
	</div>
</section>

Как видно выше, мы добавили следующие классы:

  • lg:z-[var(--stick-visibility)] увеличивает z-индекс для отображаемой секции.
  • lg:opacity-[var(--stick-visibility)] применяется к левой секции с текстом, обеспечивая прозрачность 1 для отображаемой секции и 0 для остальных. Мы также включили transition-opacity duration-300 для плавного перехода полупрозрачности.
  • lg:scale-[var(--stick-scale)] lg:opacity-[var(--stick-visibility)] добавляется к правой части каждого элемента. Эти классы следуют той же логике, что и предыдущий пункт, но также включают анимацию масштабирования для дополнительного эффекта.

На этом мы подошли к завершению нашего урока. Вы можете скачать полный код, нажав на кнопку Скачать в верхней части страницы. Несмотря на то, что код можно оптимизировать, он вполне работоспособен для реальных проектов. Не стесняйтесь адаптировать его под свои требования или экспериментировать с дополнительными переменными CSS для создания различных эффектов. Возможность творить - в ваших руках!

Выводы

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

Если вам нравятся эти современные веб-эффекты, предлагаем вам посмотреть, как создать CSS-only Card Slider with Tailwind CSS, или Infinite Horizontal Scroll Animation with Tailwind CSS.