CoreReveals and Continuous Motionmotion/react

ScrollNarrative

Scroll-driven storytelling component that synchronizes narrative chapters and visual telemetry stages with progress-linked transitions and interactive scrubbing.

Preview

Narrative Progress
35%
24 Verified Primitives

Inspect Live Motion Primitives

Test real spring physics, SVG morphs, and canvas constellations directly in the browser.

Customize

3 props
0
35

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add scroll-narrative

Writes 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.

example.tsx
1import { ScrollNarrative } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <ScrollNarrative />6  );7}

Props

PropTypeDefault
chapters

Ordered storytelling stages with title, copy, and metric.

ScrollNarrativeChapter[]DEFAULT_CHAPTERS
activeChapter

Active chapter index.

number0
progress

Simulated or scroll-linked percentage (0–100).

number35
pinStage

Whether the visual telemetry stage stays pinned alongside chapters.

booleantrue

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

components/ui/scroll-narrative.tsx
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}