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) with one extra field:

FieldTypeDefault
cssVarstring'--reveal-delay'

The computed delay is written via bindCSSVar on each element, in the order it was discovered — read it back in CSS (transition-delay: var(--reveal-delay, 0ms)) rather than in JS.

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).