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

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

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

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

Иммёрсер написан на тайпскрипте. Рантайм-бандл 9.19Кб в сжатии gzip.

Термины

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

Адаптеры для фреймворков

Для полноценного использования в современном фронтенде нужны адаптеры под фреймворки. Поэтому иммёрсер 6 был переработан под внешний рендеринг.

У-ху! React-адаптер для Immerser наконец-то релизнут: теперь разметку иммёрсера можно описывать компонентами вместо ручной сборки data-immerser-*. Vue-адаптер тоже в работе.

Установка

Через npm:

Через yarn:

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

npm install immerser

yarn add immerser

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

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

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

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

Добавьте каждому слою атрибут data-immerser-layer и уникальный id.

Также вы можете добавить ссылкам навигации атрибут data-immerser-pager-link, чтобы иммёрсер отмечал активный слой в пейджере.

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

<div data-immerser-layer id="reasoning"></div>
<div data-immerser-layer id="how-to-use"></div>
<div data-immerser-layer id="how-it-works"></div>
<div data-immerser-layer id="options"></div>
<div data-immerser-layer id="recipes"></div>

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

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

Передайте карту классов в настройке solidClassnamesByLayerId. В примере видно, как id слоя связывается с id фиксированного блока и классом, который применяется, пока блок находится поверх слоя.

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

const immerserInstance = new Immerser({
  solidClassnamesByLayerId: {
    reasoning: {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
    'how-to-use': {
      pager: 'pager--contrast-only-md',
      menu: 'menu--contrast',
      about: 'about--contrast',
    },
    'how-it-works': {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
    options: {
      logo: 'logo--contrast-only-md',
      pager: 'pager--contrast-only-md',
      language: 'language--contrast-only-md',
      menu: 'menu--contrast',
      about: 'about--contrast',
    },
    recipes: {
      logo: 'logo--contrast-lg',
      pager: 'pager--contrast-lg',
      language: 'language--contrast-lg',
    },
  },
  updateLocationHash(layerId) {
    window.history.replaceState(null, '', `#${layerId}`);
  },
  fromViewportWidth: 1024,
  pagerLinkActiveClassname: 'pager__link--active',
  scrollAdjustThreshold: 50,
  scrollAdjustDelay: 600,
  on: {
    init(immerser) {
      // колбек события инициализации
    },
    mount(immerser) {
      // колбек события монтирования
    },
    unmount(immerser) {
      // колбек события размонтирования
    },
    destroy(immerser) {
      // колбек события уничтожения
    },
    structureChange(immerser) {
      // колбек события изменения структуры DOM
    },
    layoutChange(immerser) {
      // колбек события изменения расчётов размеров слоёв
    },
    activeLayerChange(activeIndex, immerser) {
      // колбек события смены активного слоя
    },
    layerProgressChange(layerProgressArray, immerser) {
      // колбек события изменения прогресса слоёв
    },
  },
});

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

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

.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,
.language,
.about {
  color: black;
}
.pager--contrast,
.logo--contrast,
.menu--contrast,
.language--contrast,
.about--contrast {
  color: white;
}

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

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

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

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

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

При изменении размера окна или контейнера иммёрсер пересчитывает размеры и позиции. Если слои в DOM изменились, можно вызвать render(), чтобы иммёрсер заново синхронизировал структуру. Если скроллом управляет внешний скрипт, можно отключить встроенный обработчик и вызывать syncScroll() вручную.

Настройки

Вы можете передать настройки объектом в параметре конструктора.

Hot-настройки можно менять после монтирования через updateOptions. Init-настройки применяются только при инициализации.

параметртипзначение по умолчаниюhot/initописание
autoMountbooleantrueinitЕсли true, конструктор сразу монтирует иммёрсер
selectorRootunknownundefinedinitРодительский элемент только для поиска по селекторам во время монтирования
solidClassnamesByLayerIdobject{}initВложенная таблица соответствий: id слоя → id блока → CSS класс, который иммёрсер добавит этому блоку на этом слое. Пример конфигурации показан выше
fromViewportWidthnumber0hotМинимальная ширина окна в пикселях, брейкпойнт при котором иммёрсер монтируется
pagerThresholdnumber0.5hotДоля высоты окна, которую следующий слой должен занять, перед переключением навигации
updateLocationHashfunctionundefinedhotКолбек, который получает айди активного слоя при смене активного слоя. Используйте его для обновления хеша или состояния роутера
scrollAdjustThresholdnumber0hotПорог в пикселях рядом с краями секции, после которого включается подстройка скролла. Передайте ноль, чтобы отключить подстройку
scrollAdjustDelaynumber600hotЗадержка в мс перед подстройкой скролла после остановки пользовательского скролла
pagerLinkActiveClassnamestringpager-link-activeinitкласс для ссылки навигации, указывающей на активный слой
hasExternalScrollbooleanfalseinitЕсли true, иммёрсер не навешивает собственный обработчик скролла. Задуман для работы в связке с внешним скролл контроллером и вызовом метода syncScroll
hasExternalRendererbooleanfalseinitЕсли true, не запускает большую часть рутины с модификацией DOM. Задуман для работы в связке с рендер фреймворками типа React, Vue.js и другими
debugbooleanfalsehotВключает логирование предупреждений
onobject{}initНачальные обработчики событий, сгруппированные по имени события

События

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

событиеаргументыописание
initimmerser: ImmerserВызывается после инициализации
mountimmerser: ImmerserВызывается после монтирования и готовности к работе
unmountimmerser: ImmerserВызывается после размонтировании в случае, когда ширина экрана меньше переданного fromViewportWidth
destroyimmerser: ImmerserВызывается после уничтожения экземпляра
structureChangeimmerser: ImmerserВызывается после синхронизации структуры DOM
layoutChangeimmerser: ImmerserВызывается после изменений в пересчете размеров слоёв
activeLayerChangelayerIndex: number, immerser: ImmerserВызывается после изменения активного слоя
layerProgressChangelayerProgressArray: number[], immerser: ImmerserВызывается после изменения прогресса слоёв
stateChangeimmerser: ImmerserВызывается после любого события иммёрсера, чтобы внешний рендерер мог прочитать текущие публичные поля

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

названиетипописание
debugpropertyУправляет тем, сообщает ли иммёрсер о предупреждениях
mountmethodМонтирует иммёрсер, если ширина окна проходит брейкпойнт fromViewportWidth: ищет DOM, готовит разметку, рассчитывает размеры слоёв и навешивает подписчики событий
unmountmethodРазмонтирует иммёрсер: очищает разметку под управлением иммёрсера и оставляет обработку изменения ширины окна для повторного монтирования по брейкпоинту
updateOptionsmethodОбновляет runtime-настройки и применяет минимальные побочные эффекты без remount
destroymethodПолностью уничтожает иммёрсер: размонтирует его, удаляет обработку изменения ширины окна, возвращает оригинальную разметку и очищает внутреннее состояние
rendermethodПланирует синхронизацию структуры, расчеты и перерисовку после DOM-мутаций
syncScrollmethodСинхронизирует иммёрсер с внешне управляемой позицией скролла. Задуман для работы в связке с флагом hasExternalScroll=true
onmethodРегистрирует постоянный обработчик события иммёрсера
oncemethodРегистрирует одноразовый обработчик события, который удаляется после первого вызова
offmethodУдаляет конкретный обработчик для указанного события иммёрсера
activeIndexgetterИндекс активного слоя, полученный из позиции скролла
isMountedgetterПоказывает, смонтирован ли иммёрсер
rootNodegetterКорневой DOM-элемент, к которому привязан иммёрсер
layerProgressArraygetterПрогресс каждого слоя от 0 (вне экрана) до 1 (полностью видим)
layerIdsgetterId слоёв в том же порядке, в котором слои идут в DOM
structureSignaturegetterТекущая сигнатура структуры для определения изменений в списке слоёв
layoutSignaturegetterТекущая сигнатура layout для определения изменений геометрии
drawSignaturegetterТекущая сигнатура отрисовки для определения изменений визуального состояния

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

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

<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, выключите обработчик скролла иммёрсера флагом hasExternalScroll=true и вызывайте метод syncScroll при каждом обновлении позиции движком. Иммёрсер только перерисует маски и не будет вешать свой обработчик. Иммёрсер не оптимизирует вызовы в этом режиме — оптимизация производительности остается на стороне клиента.

import Immerser from 'immerser';

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

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

Внешний рендерер

Используйте hasExternalRenderer=true, когда разметкой иммёрсера управляет рендер-фреймворк. Например, React-обертка может хранить маски и блоки в состоянии фреймворка: immerser-react.

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

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

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

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

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