About
An Apple style scroll animation that scrubs an image sequence frame by frame on a canvas as you scroll, forward and backward. Ideal for product launch pages and hardware reveals.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Scrubbable Video Reveal to your project and fits it to your theme.
Code
How Scrubbable Video Reveal is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client";
import { ScrubbableVideoReveal } from "@/components/ui/scrubbable-video-reveal";
/* ───────────────────────── Scrubbable Video Reveal ───────────────────────── */
/**
* Generate 60 deterministic SVG frames so the showcase can demo a
* scrubbable sequence WITHOUT shipping image assets. In production users
* would supply 100 to 200 WebP frames.
*/
function generateScrubFrames(count: number): string[] {
return Array.from({ length: count }, (_, i) => {
const t = i / (count - 1);
const hue = Math.round(8 + t * 18);
const sat = 70;
const lit1 = Math.round(48 + t * 18);
const lit2 = Math.round(18 + t * 24);
const angle = Math.round(t * 360);
const cx = Math.round(50 + Math.sin(t * Math.PI * 2) * 18);
const cy = Math.round(50 + Math.cos(t * Math.PI * 2) * 12);
const orbR = Math.round(80 + Math.sin(t * Math.PI) * 50);
const ringR = Math.round(160 + t * 90);
const ringO = (0.18 + t * 0.4).toFixed(2);
const labelOpacity = (0.6 + t * 0.4).toFixed(2);
const svg = `<?xml version='1.0' encoding='UTF-8'?>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1280 800'>
<defs>
<radialGradient id='g' cx='${cx}%' cy='${cy}%' r='80%'>
<stop offset='0%' stop-color='hsl(${hue},${sat}%,${lit1}%)'/>
<stop offset='55%' stop-color='hsl(${hue + 4},${sat}%,${lit2}%)'/>
<stop offset='100%' stop-color='#0a0a0b'/>
</radialGradient>
<linearGradient id='l' x1='0' y1='0' x2='1' y2='1' gradientTransform='rotate(${angle} 0.5 0.5)'>
<stop offset='0%' stop-color='#cd1c18' stop-opacity='0.35'/>
<stop offset='100%' stop-color='#cd1c18' stop-opacity='0'/>
</linearGradient>
</defs>
<rect width='1280' height='800' fill='url(#g)'/>
<rect width='1280' height='800' fill='url(#l)'/>
<circle cx='640' cy='400' r='${orbR}' fill='#cd1c18' opacity='0.55'/>
<circle cx='640' cy='400' r='${orbR + 30}' fill='none' stroke='#ff8a73' stroke-opacity='0.35' stroke-width='1.5'/>
<circle cx='640' cy='400' r='${ringR}' fill='none' stroke='#ffffff' stroke-opacity='${ringO}' stroke-width='1'/>
<text x='40' y='760' font-family='ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' font-size='22' fill='#ffffff' fill-opacity='${labelOpacity}'>frame ${String(i + 1).padStart(3, "0")} / ${count}</text>
<text x='1240' y='760' text-anchor='end' font-family='ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' font-size='22' fill='#ffffff' fill-opacity='${labelOpacity}'>scroll · ${Math.round(t * 100)}%</text>
</svg>`;
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
});
}
const SCRUB_FRAMES = generateScrubFrames(60);
export function ScrubbableVideoRevealPreview() {
return (
<ScrubbableVideoReveal
frames={SCRUB_FRAMES}
aspect="16 / 10"
travel={3}
height="560px"
framed
className="w-full rounded-none border-0 bg-transparent"
/>
);
}
export default function Demo() {
return <ScrubbableVideoRevealPreview />;
}








