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.
function createRevealController(options?: RevealControllerOptions): RevealControllerimport { 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().reveal {
opacity: 0;
transition: opacity 0.4s ease-out;
transition-delay: var(--reveal-delay, 0ms);
}
.reveal.in {
opacity: 1;
}Options
RevealControllerOptions
| Field | Type | Default |
|---|---|---|
selector | string | '[data-reveal], .reveal' |
activeClass | string | null | 'in' — null disables the class toggle |
activeAttribute | string | null | null — a boolean data-attribute set alongside activeClass |
once | boolean | true — unlike useElementVisibility's own false; reveal effects almost always want this |
threshold | number | number[] | 0 |
rootMargin | string | '0px' |
root | Element | null | null |
stagger | RevealStaggerOptions | false | {} — pass false to disable |
watchMutations | boolean | true |
watchRoot | Element | document.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 |
pool | ObserverPool | shared default pool |
RevealStaggerOptions
Extends StaggerDelayOptions (step, max, unit) with one extra field:
| Field | Type | Default |
|---|---|---|
cssVar | string | '--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.
| Attribute | Overrides |
|---|---|
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 |
<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:
import { createRevealController } from '@macrulez/inview-core'
const controller = createRevealController({
stagger: { step: 60, max: 4 },
})<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).