06
Reveal on view
The IntersectionObserver ONLY writes data-attributes — data-js on mount, data-inview on first intersect, then it disconnects. Every visual is CSS gated on [data-js], so SSR, no-JS and reduced-motion render the finished layout. Scroll down.
<RevealOnView>[data-js][data-inview]reveal-staggerThe observer can only DELAY a reveal — never lose content. If JS never runs, nothing was ever hidden. If the user prefers reduced motion, the attributes are set immediately and no keyframe fires.
Solo reveal
the whole panel rises once, on the 500ms spring
This panel was in the server HTML the whole time — the wrapper only delayed its entrance until you could see it. View source: no opacity: 0 anywhere until hydration set data-js.
requests / s
18,204▲ 4.2%
p99 latency
212ms▼ 8ms
error budget
94.1%→ 0
open incidents
2▼ 1
The contract
attributes in, CSS out
useEffect(() => {
el.setAttribute("data-js", ""); // JS is alive — MAY hide now
if (reducedMotion) { el.setAttribute("data-inview", ""); return; }
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { el.setAttribute("data-inview", ""); io.disconnect(); }
}, { rootMargin });
io.observe(el);
}, []);
/* CSS — the only animator (motion.css §5) */
[data-reveal][data-js]:not([data-inview]) { opacity: 0; } /* hide AFTER hydration */
[data-reveal][data-inview] { animation: panel-in …; } /* reveal on sight */