Skip to content

Reveal Controller

createRevealController(options?) — scans the DOM for elements matching a selector and toggles a class/attribute on each as it enters the viewport, without a useElementVisibility()/observe() call per element. Built entirely on createVisibilityEngine's pooled observer — a MutationObserver (default: watching document.body) picks up elements matching selector that show up later (e.g. a v-for block rendered after an async fetch) and unobserves elements removed from the DOM.

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

const controller = createRevealController({
  selector: '[data-reveal], .reveal', // the default
  stagger: { step: 70, max: 4 },
})

// later, e.g. on app teardown
controller.destroy()
css
.reveal {
  opacity: 0;
  transition: opacity 0.4s ease-out;
  transition-delay: var(--reveal-delay, 0ms);
}
.reveal.in {
  opacity: 1;
}

Options

RevealControllerOptions

FieldTypeDefault
selectorstring'[data-reveal], .reveal'
activeClassstring | null'in'null disables the class toggle
activeAttributestring | nullnull — a boolean data-attribute set alongside activeClass
oncebooleantrue — unlike useElementVisibility's own false; reveal effects almost always want this
thresholdnumber | number[]0
rootMarginstring'0px'
rootElement | nullnull
staggerRevealStaggerOptions | false{} — pass false to disable
watchMutationsbooleantrue
watchRootElementdocument.body
onEnter / onLeave(el: Element, info: IntersectionInfo) => void— same info shape as Visibility Engine, with the matched element as the first argument since one controller tracks many
poolObserverPoolshared default pool

RevealStaggerOptions

Extends StaggerDelayOptions (step, max, unit, mode) with two extra fields:

FieldTypeDefault
cssVarstring | null'--reveal-delay' — pass null to skip writing the CSS var entirely
applyInlineDelaybooleantrue

By default the computed delay is written both as the --reveal-delay CSS var, via bindCSSVar, and as an inline transition-delay style on the element itself. The inline style is what actually wins the cascade — a .card { transition: ... } rule declared later (a hover transition, say) would otherwise silently override a transition-delay: var(--reveal-delay) CSS rule without needing !important. The CSS var keeps being written too, since unlike the inline style it's inheritable — needed for a descendant selector like .reveal > .icon { transition-delay: var(--reveal-delay) }. Pass applyInlineDelay: false to only write the CSS var, or cssVar: null to only set the inline style.

Per-element overrides

Every controller-wide option can be overridden on an individual element via a matching data-reveal-* attribute — the controller's own value only applies where the element doesn't say otherwise.

AttributeOverrides
data-reveal-once="false"once
data-reveal-threshold="0,0.5"threshold — comma-separated for an array
data-reveal-root-margin="-10%"rootMargin
data-reveal-class="visible"activeClass, for that element only
data-reveal-delay="140"an explicit stagger delay in ms, skipping the computed one entirely
data-reveal-group="grid-a"counts this element's stagger index within the named group instead of globally
html
<div class="reveal" data-reveal-once="false" data-reveal-threshold="0.4">
  Toggles back and forth instead of stopping after the first reveal.
</div>

Return value

refresh()

() => void

Re-scans selector for elements not yet observed. Rarely needed — watchMutations (on by default) already does this automatically — useful right after a synchronous DOM change if you've turned watchMutations off.

destroy()

() => void

Disconnects the MutationObserver and unobserves every currently tracked element.

Example: a page-wide reveal pass

The exact scenario this exists for — ~100 elements marked with a class across several dynamically rendered lists, some appearing after an async fetch:

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

const controller = createRevealController({
  stagger: { step: 60, max: 4 },
})
vue
<template>
  <!-- rendered before the fetch resolves — createRevealController's
       MutationObserver picks these up automatically once they render -->
  <div v-for="item in items" :key="item.id" class="reveal">
    {{ item.title }}
  </div>
</template>

No wrapper component per list item, no per-element useElementVisibility() call — the controller finds .reveal elements on its own, now and later.

When you'd reach for this instead of v-reveal/<InView>

v-reveal and <InView> (Vue) give you per-element control — a different onEnter, or the reactive isVisible value back in the template. createRevealController is for markup you'd rather not wire up element-by-element at all: one call covers the whole page (or a container), including elements that don't exist in the DOM yet. It's also the only one of the three that works the same way with no framework at all, or from React (there's no dedicated hook for it — see React Hooks).