englishпо-русски
© 2025 — Владимир Лысов, Челябинск, Россия гитхаб dubaua@gmail.com

Зачем нужен иммёрсер?

Иногда дизайнеры создают сложную логику и фиксируют части интерфейса. А еще они красят разделы страницы в контрастные цвета. Как с этим справиться?

Вам поможет иммёрсер — джаваскрипт библиотека для замены фиксированных элементов при прокрутке страницы.

Иммёрсер вычисляет состояния один раз в момент инициализации. Затем он следит за позицией скролла и планирует перерисовку документа в следующем такте цикла событий через метод requestAnimationFrame. Скрипт изменяет свойство transform, это задействует графический ускоритель.

Иммёрсер написан на тайпскрипте. Всего 6.63Кб в сжатии gzip.

Термины

Корневой элемент иммёрсера — это родительский контейнер для ваших фиксированных блоков. Фактически они позиционированы абсолютно внутри фиксированного корневого элемента. Слои — это разделы страницы, окрашенные в разные цвета. Еще вы наверняка захотите добавить навигацию по разделам, выделяющую активный раздел.

Установка

Через npm:

Через yarn:

Или если вы хотите использовать иммёрсер в браузере как глобальную переменную:

npm install immerser

yarn add immerser

<script src="https://unpkg.com/immerser@5.1.0/dist/immerser.min.js"></script>

Подготовьте разметку

Сначала настройте свой фиксированный контейнер как корневой элемент иммёрсера, добавив атрибут data-immerser

Затем расположите в нем абсолютно позиционированные дочерние элементы и добавьте каждому атрибут data-immerser-solid="solid-id" с идентификатором блока.

Добавьте каждому слою атрибут data-immerser-layer. Передайте конфигурацию в виде JSON в каждый слой с помощью атрибута data-immerser-layer-config='{"solid-id": "classname-modifier"}'. Также вы можете передать конфигурацию всех слоев массивом в параметре solidClassnameArray настроек. Конфигурация должна содержать описание классов для блоков, когда они находятся поверх слоя.

Так же вы можете добавить элемент с атрибутом data-immerser-pager для создания навигации.

<div class="fixed" data-immerser>
  <div class="fixed__pager pager" data-immerser-pager data-immerser-solid="pager"></div>
  <a href="#reasoning" class="fixed__logo logo" data-immerser-solid="logo">иммёрсер</a>
  <div class="fixed__menu menu" data-immerser-solid="menu">
    <a href="#reasoning" class="menu__link">Зачем нужен иммёрсер</a>
    <a href="#how-to-use" class="menu__link">Как пользоваться</a>
    <a href="#how-it-works" class="menu__link">Принцип работы</a>
    <a href="#options" class="menu__link">Настройки</a>
    <a href="#recipes" class="menu__link">Рецепты</a>
  </div>
  <div class="fixed__language language" data-immerser-solid="language">
    <a href="/" class="language__link">english</a>
    <a href="/ru.html" class="language__link">по-русски</a>
  </div>
  <div class="fixed__about about" data-immerser-solid="about">
    © 2025 — Владимир Лысов, Челябинск, Россия
    <a href="https://github.com/dubaua/immerser">гитхаб</a>
    <a href="mailto:dubaua@gmail.com">dubaua@gmail.com</a>
  </div>
</div>

<div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="reasoning"></div>
<div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="how-to-use"></div>
<div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="how-it-works"></div>
<div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="options"></div>
<div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="recipes"></div>

Примените стили

Добавьте стили цвета текста и фона на ваши блоки и слои с помощью классов, переданных в дата-атрибут или настройки. В примере я использую методологию БЭМ.

.fixed {
  position: fixed;
  top: 2em;
  bottom: 3em;
  left: 3em;
  right: 3em;
  z-index: 1;
}
.fixed__pager {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(0, -50%);
}
.fixed__logo {
  position: absolute;
  top: 0;
  left: 0;
}
.fixed__menu {
  position: absolute;
  top: 0;
  right: 0;
}
.fixed__language {
  position: absolute;
  bottom: 0;
  left: 0;
}
.fixed__about {
  position: absolute;
  bottom: 0;
  right: 0;
}
.pager,
.logo,
.menu,
.language,
.about {
  color: black;
}
.pager--contrast,
.logo--contrast,
.menu--contrast,
.language--contrast,
.about--contrast {
  color: white;
}

Инициализируйте иммёрсер

Добавьте иммёрсер в код и создайте экземпляр с настройками.

// Вам не нужно импортировать иммёрсер,
// если вы используете его в браузере как глобальную переменную
import Immerser from 'immerser';

const immerserInstance = new Immerser({
  // будет переопределена настройками,
  // переданными в атрибут data-immerser-layer-config каждого слоя
  solidClassnameArray: [
    {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
    {
      pager: 'pager--contrast-only-md',
      menu: 'menu--contrast',
      about: 'about--contrast',
    },
    {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
    {
      logo: 'logo--contrast-only-md',
      pager: 'pager--contrast-only-md',
      language: 'language--contrast-only-md',
      menu: 'menu--contrast',
      about: 'about--contrast',
    },
    {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
  ],
  hasToUpdateHash: true,
  fromViewportWidth: 1024,
  pagerLinkActiveClassname: 'pager__link--active',
  scrollAdjustThreshold: 50,
  scrollAdjustDelay: 600,
  on: {
    init(immerser) {
      // колбек события инициализации
    },
    bind(immerser) {
      // колбек события привязки к документу
    },
    unbind(immerser) {
      // колбек события отвязки от документа
    },
    destroy(immerser) {
      // колбек события уничтожения
    },
    activeLayerChange(activeIndex, immerser) {
      // колбек события смены активного слоя
    },
    layersUpdate(layersProgress, immerser) {
      // колбек события обновления прогресса слоёв
    },
  },
});

Принцип работы

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

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

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

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

Настройки

Вы можете передать настройки параметром функции конструктора или дата-атрибутом в документе. Дата-аттрибут обрабатывается последним, поэтому он переопределит настройки, переданные в конструктор.

параметртипзначение по умолчаниюописание
solidClassnameArrayarray[]Массив настроек слоев. Конфигурация, переданная в data-immerser-layer-config перезапишет эту настройку для соответствующего слоя. Пример конфигурации показан выше
fromViewportWidthnumber0Минимальная ширина окна для инициализации иммёрсера
pagerThresholdnumber0.5Насколько должен следующий слой быть видим в окне, чтобы он стал активен в навигации
hasToUpdateHashbooleanfalseФлаг, контролирующий обновление хеша страницы
scrollAdjustThresholdnumber0Дистанция до верха или низа окна браузера в пикселях. Если текущая дистанция меньше переданного значения, то скрипт подстроит положение скролла
scrollAdjustDelaynumber600Сколько ждать бездействия пользователя, чтобы начать подстройку скролла
pagerLinkActiveClassnamestringpager-link-activeПрименяется, к каждой ссылке пейджера, ссылающуюся на активный слой
isScrollHandledbooleantrueПодписывается на событие прокрутки, если включено. Выключите, если используете внешний контроллер скролла
debugbooleanfalseВключает логирование предупреждений и ошибок. По умолчанию true в режиме разработки, иначе false
onobject{}Начальные обработчики событий, сгруппированные по имени события

События

На события можно подписаться через поле on в настройках конструктора или с помощью вызова метода on или once у экземпляра иммёрсера.

событиеаргументыописание
initimmerser: ImmerserВызывается после инициализации
bindimmerser: ImmerserВызывается после привязки к DOM
unbindimmerser: ImmerserВызывается после отвязки от DOM
destroyimmerser: ImmerserВызывается после уничтожения экземпляра
activeLayerChangelayerIndex: number, immerser: ImmerserВызывается при смене активного слоя
layersUpdatelayersProgress: number[], immerser: ImmerserВызывается при каждом обновлении скролла

Публичные поля и методы

названиетипописание
debugpropertyУправляет тем, логирует ли иммёрсер предупреждения и ошибки
bindmethodКлонирует разметку, навешивает подписчики и запускает логику
unbindmethodудаляет сгенерированную разметку и подписчики, сохраняя экземпляр пригодным к повторному использованию
destroymethodПолностью уничтожает иммёрсер: отключает его, удаляет слушатели, возвращает оригинальную разметку и очищает внутреннее состояние
rendermethodПересчитывает размеры и перерисовывает маски
syncScrollmethodОбновляет иммёрсер при внешнем управлении скроллом (требуется флаг isScrollHandled = false)
onmethodРегистрирует постоянный обработчик события иммёрсера
oncemethodРегистрирует одноразовый обработчик события, который удаляется после первого вызова
offmethodУдаляет конкретный обработчик для указанного события иммёрсера
activeIndexgetterТекущий индекс активного слоя, рассчитанный по позиции скролла
isBoundgetterПоказывает, активен ли сейчас иммёрсер (разметка склонирована, подписчики навешаны)
rootNodegetterКорневой элемент, к которому привязан иммёрсер
layerProgressArraygetterПрогресс каждого слоя (0–1), показывающий, насколько слой виден в окне браузера

Клонирование подписчиков

Вы уже знаете, что иммёрсер клонирует элементы. Подписчики событий и данные, привязанные к нодам, не клонируются вместе с элементом. К счастью, вы можете разметить иммёрсер самостоятельно. Для этого разместите внутри корневого элемента маскирующие контейнеры для блоков по числу слоев. В таком случае скрипт не будет клонировать элементы. Подписчики и реактивная логика останутся нетронутыми. В примере на этой странице я создаю подписчик на клик по смайлу справа до инициализации.

<div class="fixed" data-immerser>
  <div data-immerser-mask>
    <div data-immerser-mask-inner>
      <!-- ваша разметка -->
    </div>
  </div>
  <div data-immerser-mask>
    <div data-immerser-mask-inner>
      <!-- ваша разметка -->
    </div>
  </div>
</div>

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

Если вы наведете мышь на элемент, находящийся на границе слоев, то псевдоселектор :hover сработает только на одну часть. Чтобы наведение сработало на все клоны элемента, задайте идентификатор наведения в атрибуте data-immerser-synchro-hover="hoverId". При наведении мыши на такой элемент, ко всем его клонам добавится класс _hover. Стилизуйте по этому селектору вместе с псевдоселектором :hover, чтобы добиться нужного эффекта.

a:hover,
a._hover {
  color: magenta;
}

Обработка изменения документа

Иммёрсер не отслеживает изменения документа, если вы динамически добавляете или удаляете ноды. Если вы меняете высоту документа, и хотите, чтобы иммёрсер пересчитал и перерисовал блоки, вызовите метод render у экземпляра иммёрсера.

// проведите любые манипуляции, меняющие поток документа
document.appendChild(someNode);
document.removeChild(anotherNode);

// затем укажите иммёрсеру перерисовать штуки
immerserInstance.render();

Внешний скролл-движок

Если прокруткой управляет кастомный движок, например, Locomotive Scroll, выключите обработчик скролла иммёрсера флагом isScrollHandled=false и вызывайте метод syncScroll при каждом обновлении позиции движком. Иммёрсер только перерисует маски и не будет вешать свой обработчик. Иммёрсер не оптимизирует вызовы в этом режиме — оптимизация производительности остается на стороне клиента.

import Immerser from 'immerser';

const immerserInstance = new Immerser({
  // выключаем обработку скролла иммёрсером под внешний движок
  isScrollHandled: false,
});

customScrollEngine.on('scroll', () => {
  // подпишитесь на событие скролла движка и запустите синхронизацию иммёрсера
  immerserInstance.syncScroll();
});

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

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

Для меня ИИ — это инструмент наравне с линтерами, сборщиками и другими средствами ускорения и упрощения работы. Я ленивый, и моя лень толкает меня на изобретения.

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