View Timeline Reveal
Scroll reveals on a real CSS view timeline — off the main thread, and visible by default where it is not supported.
DOM + CSS0 deps1 file · 3 KB TSXreduced-motion aware
Props
"use client";
import { useId, type ReactNode } from "react";
/**
* Scroll-linked reveals on a CSS VIEW TIMELINE — no observer, no rAF, no JS at
* all once the stylesheet is in.
*
* The reason to reach for this over an IntersectionObserver is not tidiness:
* scroll-driven animations are driven off the main thread. A JS reveal competes
* with hydration, image decode and everything else on the thread, which is
* exactly when a long page is being scrolled fast.
*/
export function ViewTimelineReveal({
children,
distance = 28,
start = 8,
end = 34,
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this animation
Scroll reveals have been an IntersectionObserver and a class toggle for a decade, and CSS scroll-driven animations replace both — not because they are tidier but because they are driven off the main thread, which matters precisely when a long page is being scrolled quickly and the thread is busy with hydration and image decode. Using them well comes down to one rule about where the hidden state lives. The tempting shape is to set opacity to zero on the element and let the animation bring it back; that turns every browser without support into a page of invisible content, which is the worst outcome a progressive enhancement can produce. Confining the start state to the animation's own from-keyframe, filling in both directions, and wrapping the declaration in a feature query inverts it: the element is visible by default and the hidden state only ever exists where the animation actually runs. The range is expressed with the named timeline ranges rather than pixels, so it stays correct at every viewport size, and the whole declaration sits inside a reduced-motion query so the animation is opt-in — which means reduced motion needs no separate branch at all, since the element is already visible and simply stays that way. Class and keyframe names are scoped per instance, or two wrappers with different ranges quietly share one timing.
Curator’s note
Put opacity:0 on the element instead of in the keyframe and every unsupported browser gets a blank page. That is the whole lesson.
