Иногда дизайнеры создают сложную логику и фиксируют части интерфейса. А еще они красят разделы страницы в контрастные цвета. Как с этим справиться?
Вам поможет иммёрсер — джаваскрипт библиотека для замены фиксированных элементов при прокрутке страницы.
Иммёрсер написан на тайпскрипте. Рантайм-бандл 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 | описание |
|---|---|---|---|---|
| autoMount | boolean | true | init | Если true, конструктор сразу монтирует иммёрсер |
| selectorRoot | unknown | undefined | init | Родительский элемент только для поиска по селекторам во время монтирования |
| solidClassnamesByLayerId | object | {} | init | Вложенная таблица соответствий: id слоя → id блока → CSS класс, который иммёрсер добавит этому блоку на этом слое. Пример конфигурации показан выше |
| fromViewportWidth | number | 0 | hot | Минимальная ширина окна в пикселях, брейкпойнт при котором иммёрсер монтируется |
| pagerThreshold | number | 0.5 | hot | Доля высоты окна, которую следующий слой должен занять, перед переключением навигации |
| updateLocationHash | function | undefined | hot | Колбек, который получает айди активного слоя при смене активного слоя. Используйте его для обновления хеша или состояния роутера |
| scrollAdjustThreshold | number | 0 | hot | Порог в пикселях рядом с краями секции, после которого включается подстройка скролла. Передайте ноль, чтобы отключить подстройку |
| scrollAdjustDelay | number | 600 | hot | Задержка в мс перед подстройкой скролла после остановки пользовательского скролла |
| pagerLinkActiveClassname | string | pager-link-active | init | класс для ссылки навигации, указывающей на активный слой |
| hasExternalScroll | boolean | false | init | Если true, иммёрсер не навешивает собственный обработчик скролла. Задуман для работы в связке с внешним скролл контроллером и вызовом метода syncScroll |
| hasExternalRenderer | boolean | false | init | Если true, не запускает большую часть рутины с модификацией DOM. Задуман для работы в связке с рендер фреймворками типа React, Vue.js и другими |
| debug | boolean | false | hot | Включает логирование предупреждений |
| on | object | {} | init | Начальные обработчики событий, сгруппированные по имени события |
На события можно подписаться через поле on в настройках конструктора или с помощью вызова метода on или once у экземпляра иммёрсера.
| событие | аргументы | описание |
|---|---|---|
| init | immerser: Immerser | Вызывается после инициализации |
| mount | immerser: Immerser | Вызывается после монтирования и готовности к работе |
| unmount | immerser: Immerser | Вызывается после размонтировании в случае, когда ширина экрана меньше переданного fromViewportWidth |
| destroy | immerser: Immerser | Вызывается после уничтожения экземпляра |
| structureChange | immerser: Immerser | Вызывается после синхронизации структуры DOM |
| layoutChange | immerser: Immerser | Вызывается после изменений в пересчете размеров слоёв |
| activeLayerChange | layerIndex: number, immerser: Immerser | Вызывается после изменения активного слоя |
| layerProgressChange | layerProgressArray: number[], immerser: Immerser | Вызывается после изменения прогресса слоёв |
| stateChange | immerser: Immerser | Вызывается после любого события иммёрсера, чтобы внешний рендерер мог прочитать текущие публичные поля |
| название | тип | описание |
|---|---|---|
| debug | property | Управляет тем, сообщает ли иммёрсер о предупреждениях |
| mount | method | Монтирует иммёрсер, если ширина окна проходит брейкпойнт fromViewportWidth: ищет DOM, готовит разметку, рассчитывает размеры слоёв и навешивает подписчики событий |
| unmount | method | Размонтирует иммёрсер: очищает разметку под управлением иммёрсера и оставляет обработку изменения ширины окна для повторного монтирования по брейкпоинту |
| updateOptions | method | Обновляет runtime-настройки и применяет минимальные побочные эффекты без remount |
| destroy | method | Полностью уничтожает иммёрсер: размонтирует его, удаляет обработку изменения ширины окна, возвращает оригинальную разметку и очищает внутреннее состояние |
| render | method | Планирует синхронизацию структуры, расчеты и перерисовку после DOM-мутаций |
| syncScroll | method | Синхронизирует иммёрсер с внешне управляемой позицией скролла. Задуман для работы в связке с флагом hasExternalScroll=true |
| on | method | Регистрирует постоянный обработчик события иммёрсера |
| once | method | Регистрирует одноразовый обработчик события, который удаляется после первого вызова |
| off | method | Удаляет конкретный обработчик для указанного события иммёрсера |
| activeIndex | getter | Индекс активного слоя, полученный из позиции скролла |
| isMounted | getter | Показывает, смонтирован ли иммёрсер |
| rootNode | getter | Корневой DOM-элемент, к которому привязан иммёрсер |
| layerProgressArray | getter | Прогресс каждого слоя от 0 (вне экрана) до 1 (полностью видим) |
| layerIds | getter | Id слоёв в том же порядке, в котором слои идут в DOM |
| structureSignature | getter | Текущая сигнатура структуры для определения изменений в списке слоёв |
| layoutSignature | getter | Текущая сигнатура layout для определения изменений геометрии |
| drawSignature | getter | Текущая сигнатура отрисовки для определения изменений визуального состояния |
Вы уже знаете, что иммёрсер клонирует элементы. Подписчики событий и данные, привязанные к нодам, не клонируются вместе с элементом. К счастью, вы можете разметить иммёрсер самостоятельно. Для этого разместите внутри корневого элемента маскирующие контейнеры для блоков по числу слоёв. В таком случае скрипт не будет клонировать элементы. Подписчики и реактивная логика останутся нетронутыми. В примере на этой странице я создаю подписчик на клик по смайлу справа до инициализации.
<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 году до появления программирования с ИИ. В более поздних итерациях ИИ использовался как вспомогательный инструмент для инфраструктурных задач, обновления документации и генерации обслуживающего кода.
Для меня ИИ — это инструмент наравне с линтерами, сборщиками и другими средствами ускорения и упрощения работы. Я ленивый, и моя лень толкает меня на изобретения.
Я использую ИИ открыто и считаю важным прямо об этом говорить, потому что для кого-то это может иметь решающее значение.