Scroll-driven storytelling component that synchronizes narrative chapters and visual telemetry stages with progress-linked transitions and interactive scrubbing.
Preview
Inspect Live Motion Primitives
Test real spring physics, SVG morphs, and canvas constellations directly in the browser.
Customize
3 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add scroll-narrativeWrites components/ui/scroll-narrative.tsx and components/styles/scroll-narrative.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { ScrollNarrative } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <ScrollNarrative />6 );7}Props
Accessibility
Provides accessible chapter buttons and a progress scrubber so keyboard and reduced-motion users can navigate every narrative stage without mandatory scroll-jacking.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/scroll-narrative.module.css";6 7export interface ScrollNarrativeChapter {8 id: string;9 step: string;10 title: string;11 summary: string;12 metric: string;13}14 15export interface ScrollNarrativeProps {16 /** Narrative chapters */17 chapters?: ScrollNarrativeChapter[];18 /** Active chapter index (0 to chapters.length - 1) */19 activeChapter?: number;20 /** Progress percentage (0 to 100) */21 progress?: number;22 /** Pin visual telemetry card */23 pinStage?: boolean;24 /** Optional className */25 className?: string;26}27 28const DEFAULT_CHAPTERS: ScrollNarrativeChapter[] = [29 {30 id: "discover",31 step: "01 // DISCOVER",32 title: "Inspect Live Motion Primitives",33 summary: "Test real spring physics, SVG morphs, and canvas constellations directly in the browser.",34 metric: "24 Verified Primitives",35 },36 {37 id: "customize",38 step: "02 // CUSTOMIZE",39 title: "Tune Physics & Tokens",40 summary: "Adjust stiffness, damping, blur, and speed while watching the JSX usage code update live.",41 metric: "Zero Lock-In Source",42 },43 {44 id: "ship",45 step: "03 // DEPLOY",46 title: "Install via CLI or Package",47 summary: "Run npx motionfoundry add or import named primitives from @motionfoundry/ui.",48 metric: "100% TypeScript Typed",49 },50];51 52export function ScrollNarrative({53 chapters = DEFAULT_CHAPTERS,54 activeChapter = 0,55 progress = 35,56 pinStage = true,57 className = "",58}: ScrollNarrativeProps) {59 const [currentIdx, setCurrentIdx] = React.useState(activeChapter);60 const prefersReducedMotion = useReducedMotion();61 62 React.useEffect(() => {63 setCurrentIdx(Math.min(Math.max(activeChapter, 0), chapters.length - 1));64 }, [activeChapter, chapters.length]);65 66 const current = chapters[currentIdx] ?? chapters[0];67 68 return (69 <div className={`${styles.container} ${className}`.trim()}>70 <div className={styles.progressHeader}>71 <span className={styles.progressLabel}>Narrative Progress</span>72 <div className={styles.progressBar}>73 <div74 className={styles.progressFill}75 style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}76 />77 </div>78 <span className={styles.progressValue}>{progress}%</span>79 </div>80 81 <div className={styles.grid}>82 <div className={styles.chapterList} role="tablist" aria-label="Storytelling chapters">83 {chapters.map((ch, idx) => {84 const active = idx === currentIdx;85 return (86 <button87 key={ch.id}88 type="button"89 role="tab"90 aria-selected={active}91 onClick={() => setCurrentIdx(idx)}92 className={`${styles.chapterItem} ${active ? styles.chapterActive : ""}`.trim()}93 >94 <span className={styles.stepTag}>{ch.step}</span>95 <span className={styles.chapterTitle}>{ch.title}</span>96 </button>97 );98 })}99 </div>100 101 {pinStage && current && (102 <motion.div103 key={current.id}104 initial={prefersReducedMotion ? false : { opacity: 0, y: 12 }}105 animate={{ opacity: 1, y: 0 }}106 transition={{ duration: 0.38, ease: [0.16, 1, 0.3, 1] }}107 className={styles.visualStage}108 >109 <span className={styles.metricPill}>{current.metric}</span>110 <h4 className={styles.stageHeadline}>{current.title}</h4>111 <p className={styles.stageSummary}>{current.summary}</p>112 </motion.div>113 )}114 </div>115 </div>116 );117}