Issue №126 Summer 2026A journal of dev tools, libraries & small ideasUpdated weekly
frontend · 4 min read
frontendMay 28, 2026 · 4 min

scroll-reveal-kit

Drop-in IntersectionObserver reveal animations

scroll-reveal-kit — live demo screenshot
01
frontend

scroll-reveal-kit: scroll reveals that should have been a one-liner

For something so common, "fade this in when it scrolls into view" is weirdly annoying to do in React.

You can reach for a full animation library like Framer Motion or GSAP, but you're now shipping 30–50 KB and adopting a new mental model for what was supposed to be a styling concern. Or you write your own IntersectionObserver hook — for the 50th time — and a week later you discover it silently broke under React 18 StrictMode, or it didn't unobserve when the element unmounted, or it doesn't respect prefers-reduced-motion.

I got tired of doing this from scratch, so I made the in-between: scroll-reveal-kit. One <Reveal> component, one useScrollReveal hook, eleven presets, zero dependencies.

import { Reveal } from 'scroll-reveal-kit';

<Reveal variant="fade-up">
  <h2>Hello, scroll.</h2>
</Reveal>;

That's the whole API for 90% of cases.

What's in the box

  • Eleven variants: fade, fade-up, fade-down, fade-left, fade-right, zoom-in, zoom-out, flip-up, flip-down, slide-up, rotate.
  • delay / duration / easing props for staggered groups and timing tweaks. Easing is just a CSS string, so anything you'd put in a transition: value works.
  • once toggle — reveal once and stop observing (default), or toggle whenever the element enters/leaves the viewport.
  • prefers-reduced-motion by default — users who opt out of motion at the OS level get content instantly, no transitions, no opacity dance.
  • Polymorphic as prop<Reveal as="section">, <Reveal as={Card}>, whatever you want.
  • Full TypeScript types, including the RevealVariant union for autocomplete.

How I built it (the two interesting bits)

The React 19 StrictMode trap

The first version of this used a ref callback to set up the IntersectionObserver and a separate useEffect for cleanup. It passed every test in jsdom. It worked great in storybook. The moment I dropped it into a real React 19 app wrapped in <StrictMode>, it stopped working — every reveal stayed permanently hidden.

What happens: React 19 in dev simulates an unmount/remount cycle to surface side-effect bugs. During that cycle, the useEffect cleanup runs (killing the observer) but the ref callback does not re-fire on the simulated remount. Your observer is gone, with no signal that anything went wrong. jsdom doesn't simulate this dance, so tests pass.

The fix is to track the node via useState and put the entire observer lifecycle inside a single useEffect:

const [node, setNode] = useState<Element | null>(null);
const ref = useCallback((n: Element | null) => setNode(n), []);

useEffect(() => {
  if (!node) return;
  const observer = new IntersectionObserver(/* ... */);
  observer.observe(node);
  return () => observer.disconnect();
}, [node /* + other deps */]);

Now setup and teardown both live in the useEffect, and React handles the StrictMode replay correctly. This is the pattern I'd recommend for any browser-API-bound hook — ResizeObserver, MutationObserver, anything with a setup/teardown cycle. The ref callback's job is purely to expose the node to React state.

CSS transitions, not a JS animation engine

I considered building a tiny tween system, but every scroll-reveal library I actually like has the same realization: the browser already has a perfectly good transition engine, it's GPU-accelerated, and it interrupts cleanly. So <Reveal> just toggles between two style objects — one "from" state and one "to" state — and lets transition: opacity ..., transform ... do the work.

The win: no animation frame loop, no scheduling overhead per element, and the transitions happily interrupt themselves if once={false} toggles you back out mid-fade. Total runtime cost per element is "one IntersectionObserver" — which the browser is already great at batching.

Install and try it

pnpm add scroll-reveal-kit

Live demo with every variant scrolling past you at once: scroll-reveal-kit.vercel.app

Source code on GitHub: github.com/kea0811/scroll-reveal-kit

What's next

A few things on the list:

  • A useStaggered helper so you don't have to write delay={i * 120} by hand.
  • Optional viewportEnter / viewportLeave callbacks for triggering side effects (analytics, video play/pause).
  • Maybe a Vue port — same hook pattern is portable.

If you find a case where the defaults feel off, or a variant you wish existed, open an issue. This is the kind of library that gets better one paper cut at a time.


End of essay

About the author
Er An Khoo