Skip to content

Контроллер reveal-эффектов ​

createRevealController(options?) — сканирует DOM на элементы, подходящие под селектор, и переключает класс/атрибут на каждом по мере появления во viewport, без вызова useElementVisibility()/ observe() на каждый элемент. Построен целиком поверх пулящегося обсервера createVisibilityEngine — MutationObserver (по умолчанию следит за document.body) подхватывает элементы, подходящие под selector, появившиеся позже (например, блок v-for, отрендеренный после асинхронного запроса), и снимает наблюдение с элементов, удалённых из DOM.

ts
function createRevealController(options?: RevealControllerOptions): RevealController
ts
import { createRevealController } from '@macrulez/inview-core'

const controller = createRevealController({
  selector: '[data-reveal], .reveal', // значение по умолчанию
  stagger: { step: 70, max: 4 },
})

// позже, например при демонтаже приложения
controller.destroy()
css
.reveal {
  opacity: 0;
  transition: opacity 0.4s ease-out;
  transition-delay: var(--reveal-delay, 0ms);
}
.reveal.in {
  opacity: 1;
}

Опции ​

RevealControllerOptions ​

ПолеТипПо умолчанию
selectorstring'[data-reveal], .reveal'
activeClassstring | null'in' — null отключает переключение класса
activeAttributestring | nullnull — булев data-атрибут, выставляется вместе с activeClass
oncebooleantrue — в отличие от false у useElementVisibility; reveal-эффектам это почти всегда нужно
thresholdnumber | number[]0
rootMarginstring'0px'
rootElement | nullnull
staggerRevealStaggerOptions | false{} — false отключает
watchMutationsbooleantrue
watchRootElementdocument.body
onEnter / onLeave(el: Element, info: IntersectionInfo) => void— та же форма info, что в Движке видимости, первым аргументом — сам элемент, потому что один контроллер следит за многими
poolObserverPoolобщий пул по умолчанию

RevealStaggerOptions ​

Расширяет StaggerDelayOptions (step, max, unit, mode) двумя дополнительными полями:

ПолеТипПо умолчанию
cssVarstring | null'--reveal-delay' — null полностью отключает запись CSS-переменной
applyInlineDelaybooleantrue

По умолчанию вычисленная задержка записывается сразу двумя способами: как CSS-переменная --reveal-delay через bindCSSVar, и как инлайн-стиль transition-delay прямо на элементе. Именно инлайн-стиль реально побеждает в каскаде — правило вроде .card { transition: ... }, объявленное позже (например, transition при hover), иначе молча перебило бы transition-delay: var(--reveal-delay) без !important. CSS-переменная при этом продолжает записываться — она, в отличие от инлайн-стиля, наследуется, что нужно для селектора потомка вроде .reveal > .icon { transition-delay: var(--reveal-delay) }. Передайте applyInlineDelay: false, чтобы записывать только CSS-переменную, или cssVar: null, чтобы выставлять только инлайн-стиль.

Задержка стаггера полностью пропускается — без CSS-переменной, без инлайн-стиля, без слушателя очистки — когда prefersReducedMotion() возвращает true, так же как при явном stagger: false. Само переключение класса/атрибута при этом всё равно происходит как обычно — вопрос движения касается только таймингов стаггера.

Переопределения на уровне элемента ​

Любую опцию контроллера можно переопределить на конкретном элементе через соответствующий data-reveal-* атрибут — значение контроллера применяется только там, где элемент не сказал иначе.

АтрибутПереопределяет
data-reveal-once="false"once
data-reveal-threshold="0,0.5"threshold — через запятую для массива
data-reveal-root-margin="-10%"rootMargin
data-reveal-class="visible"activeClass, только для этого элемента
data-reveal-attribute="active"activeAttribute, только для этого элемента
data-reveal-delay="140"явная задержка стаггера в мс, полностью пропуская вычисленную
data-reveal-group="grid-a"индекс стаггера считается внутри названной группы, а не глобально
html
<div class="reveal" data-reveal-once="false" data-reveal-threshold="0.4">
  Переключается туда-обратно, вместо того чтобы остановиться после первого показа.
</div>

Возвращаемое значение ​

refresh(selector?) ​

(selector?: string) => void

Повторно сканирует на ещё не отслеживаемые элементы. Редко нужен — watchMutations (включён по умолчанию) уже делает это автоматически — полезен сразу после синхронного изменения DOM, если watchMutations выключен. Передайте селектор, чтобы отсканировать этим конкретным вызовом что-то отличное от собственного selector контроллера (например, более узкий, ограниченный только что отрендеренным поддеревом), не меняя при этом, что ищет сам watchMutations дальше.

observe(el) ​

(el: Element) => void

Начинает наблюдать за el напрямую, минуя скан по selector — для элемента, на который уже есть ссылка и для которого не хочется полагаться на совпадение с селектором. Не делает ничего, если el уже наблюдается.

unobserve(el) ​

(el: Element) => void

Прекращает наблюдение за el, так же как если бы он был удалён из DOM.

destroy() ​

() => void

Отключает MutationObserver и снимает наблюдение со всех отслеживаемых на данный момент элементов.

Пример: reveal-проход по всей странице ​

Тот самый сценарий, для которого всё это существует — около 100 элементов с классом на нескольких динамически рендерящихся списках, часть из которых появляется после асинхронного запроса:

ts
import { createRevealController } from '@macrulez/inview-core'

const controller = createRevealController({
  stagger: { step: 60, max: 4 },
})
vue
<template>
  <!-- отрендерено до завершения запроса — MutationObserver контроллера
       сам подхватит эти элементы, как только они появятся -->
  <div v-for="item in items" :key="item.id" class="reveal">
    {{ item.title }}
  </div>
</template>

Никакого компонента-обёртки на каждый элемент списка, никакого вызова useElementVisibility() на каждый элемент — контроллер сам находит .reveal-элементы, сейчас и позже.

Обёртка жизненного цикла: useRevealController ​

И у Vue, и у React есть тонкая обёртка жизненного цикла, убирающая ручной шаблонный код onMounted/useEffect + destroy(), который иначе нужен для контроллера на всю страницу в каждом месте вызова — создаёт контроллер при монтировании, уничтожает автоматически при размонтировании и возвращает ту же форму refresh/observe/unobserve/destroy:

ts
function useRevealController(options?: RevealControllerOptions): RevealController
vue
<script setup lang="ts">
import { useRevealController } from '@macrulez/inview-vue'

useRevealController({ stagger: { step: 70, max: 4 } })
</script>
tsx
import { useRevealController } from '@macrulez/inview-react'

function App() {
  useRevealController({ stagger: { step: 70, max: 4 } })
  return <Page />
}

options захватываются один раз, при монтировании, и используются на весь срок жизни контроллера — та же одноразовая модель опций, что и у самого createRevealController, а не что-то, реагирующее на изменение объекта options при каждом ре-рендере/повторном вызове.

Когда лучше v-reveal/<InView>, а не это ​

v-reveal и <InView> (Vue) дают контроль на уровне отдельного элемента — свой onEnter для каждого, или реактивный isVisible прямо в шаблоне. createRevealController — для разметки, которую не хочется размечать поэлементно вообще: один вызов покрывает всю страницу (или контейнер), включая элементы, которых ещё нет в DOM. Это также единственный из трёх вариантов, который работает одинаково вообще без фреймворка — про React смотрите React-хуки (там же useRevealController выше, либо прямой вызов createRevealController).