Scrolly Stage
moco/scrollyFreeComponent
Scrollytelling stage: a pinned figure that updates as prose steps scroll past, unpinning below 1024px.
Live preview
live · rendered by the registry
Rendered by moco on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://mocoui.site/r/scrolly.jsonSource
1"use client";23import * as React from "react";4import "./scrolly.css";56export interface StepShape {7 key: string;8 caption: string;9 text: React.ReactNode;10}1112export interface ScrollyStageProps<S extends StepShape> {13 steps: S[];14 render: (step: S, index: number, progress: number) => React.ReactNode;15 initial?: number;16}1718/**19 * A figure that pins to the viewport while prose steps scroll past it.20 * Below 1024px it un-pins and each step renders its own static snapshot.21 * With JS off, every step is visible and the figure shows the final state22 * (add a `no-js` class to <html> and remove it with JS to get that fallback).23 */24export function ScrollyStage<S extends StepShape>({25 steps,26 render,27 initial = 0,28}: ScrollyStageProps<S>) {29 const [active, setActive] = React.useState(initial);30 const refs = React.useRef<(HTMLDivElement | null)[]>([]);3132 React.useEffect(() => {33 const io = new IntersectionObserver(34 (entries) => {35 for (const e of entries) {36 if (!e.isIntersecting) continue;37 const i = Number((e.target as HTMLElement).dataset.step);38 if (!Number.isNaN(i)) setActive(i);39 }40 },41 // Fire when a step crosses the middle band of the viewport.42 { rootMargin: "-45% 0px -45% 0px", threshold: 0 },43 );44 refs.current.forEach((el) => el && io.observe(el));45 return () => io.disconnect();46 }, [steps.length]);4748 const current = steps[Math.min(active, steps.length - 1)];4950 return (51 <div className="moco-scrolly">52 <div className="moco-scrolly-figure" aria-hidden="true">53 <div className="moco-plate">54 {render(current, active, active / Math.max(1, steps.length - 1))}55 </div>56 <p className="moco-step-cap">{current.caption}</p>57 </div>5859 <div className="moco-scrolly-steps">60 {steps.map((s, i) => (61 <div62 key={s.key}63 data-step={i}64 data-on={i === active}65 className="moco-scrolly-step"66 ref={(el) => {67 refs.current[i] = el;68 }}69 >70 {/* Mobile / no-JS snapshot, above its own paragraph. */}71 <div className="moco-scrolly-inline">72 <div className="moco-plate">73 {render(s, i, i / Math.max(1, steps.length - 1))}74 </div>75 <p className="moco-step-cap">{s.caption}</p>76 </div>77 <div>{s.text}</div>78 </div>79 ))}80 </div>81// … truncated
What it pulls in
Other registry items
Files it writes
More from moco
All 20 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Codecode | moco | Components | Free | 1 dep · 3 files | |
| Comparecompare | moco | Components | Free | no deps · 2 files | |
| Count Upcountup | moco | Components | Free | no deps · 2 files | |
| Covercover | moco | Components | Free | no deps · 2 files | |
| Diagram Kitdiagram | moco | Components | Free | no deps · 2 files | |
| Dot Griddotgrid | moco | Components | Free | no deps · 2 files | |
| Figurefigure | moco | Components | Free | no deps · 2 files | |
| Hookshooks | moco | Components | Free | no deps |
