npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@matthesketh/react-storyteller

v1.2.1

Published

React 19 scroll-driven storytelling. Three layouts, step callbacks, smooth progress tracking, zero dependencies.

Readme

npm version npm downloads license TypeScript React zero deps demo

@matthesketh/react-storyteller

Scroll-driven storytelling for React 19. Three layout modes, step callbacks, smooth progress tracking -- zero runtime dependencies.

Live Demo  |  npm

Features

  • Three layouts: side-by-side, overlay, and stacked
  • Step callbacks: onEnter and onExit per step or globally
  • Progress tracking: smooth 0–1 value within the active step
  • Zero dependencies: no runtime deps beyond React itself
  • TypeScript-first: full type definitions included
  • React 19 native: built against React 19 with no compatibility shims
  • Minimal CSS: opt-in stylesheet, fully overridable

Install

npm install @matthesketh/react-storyteller

Quick Start

import { Storyteller } from '@matthesketh/react-storyteller';
import '@matthesketh/react-storyteller/styles';

const steps = [
  { content: <p>Step one: introduce the subject.</p> },
  { content: <p>Step two: show the change.</p> },
  { content: <p>Step three: reveal the outcome.</p> },
];

export function MyStory() {
  return (
    <Storyteller
      steps={steps}
      graphic={({ stepIndex }) => (
        <div className="graphic">
          <strong>Step {stepIndex + 1}</strong>
        </div>
      )}
    />
  );
}

API

StorytellerProps

| Prop | Type | Default | Description | |---|---|---|---| | steps | StorytellerStep[] | — | Array of step definitions | | graphic | (props: GraphicRenderProps) => ReactNode | — | Render prop for the sticky graphic panel | | layout | 'side-by-side' \| 'overlay' \| 'stacked' | 'side-by-side' | Layout mode | | progress | boolean | false | Enable per-step scroll progress tracking | | snap | boolean \| 'proximity' \| 'mandatory' | false | Enable CSS scroll-snap on steps | | hashSync | boolean \| string | false | Sync active step to URL hash (#step-1) | | offset | number | 0.5 | Viewport trigger point (0 = top, 1 = bottom) | | onStepEnter | (context: StepContext) => void | — | Fires when any step enters the trigger point | | onStepExit | (context: StepContext) => void | — | Fires when any step exits the trigger point | | className | string | — | Class added to the outer container element | | graphicClassName | string | — | Class added to the graphic panel element | | stepClassName | string | — | Class added to every step element | | activeStepClassName | string | 'is-active' | Class added to the currently active step element |

StorytellerStep

| Property | Type | Required | Description | |---|---|---|---| | content | ReactNode | Yes | The content rendered inside the step | | onEnter | (context: StepContext) => void | No | Fires when this step enters the trigger point | | onExit | (context: StepContext) => void | No | Fires when this step exits the trigger point | | className | string | No | Additional class applied to this step's element |

GraphicRenderProps

| Property | Type | Description | |---|---|---| | stepIndex | number | Index of the currently active step (-1 before any step is active) | | progress | number | Scroll progress within the active step (0–1); requires progress prop | | direction | ScrollDirection | Last scroll direction: 'up' or 'down' |

StepContext

| Property | Type | Description | |---|---|---| | index | number | Index of the step that triggered the callback | | direction | ScrollDirection | Scroll direction at the time of the event | | progress | number | Scroll progress at the time of the event |

Layouts

side-by-side (default)

The graphic is sticky on one side while steps scroll past on the other. Best for data visualisations and annotated diagrams.

<Storyteller
  layout="side-by-side"
  steps={steps}
  graphic={({ stepIndex }) => <Chart activeIndex={stepIndex} />}
/>

overlay

The graphic fills the full viewport and the steps scroll over it. Best for full-bleed imagery or video backgrounds.

<Storyteller
  layout="overlay"
  steps={steps}
  graphic={({ stepIndex }) => <FullBleedImage src={images[stepIndex]} />}
/>

stacked

The graphic sits above the steps in normal document flow — no sticky positioning. Best for mobile-first layouts or simpler stories where a fixed panel is not needed.

<Storyteller
  layout="stacked"
  steps={steps}
  graphic={({ stepIndex }) => <Summary index={stepIndex} />}
/>

Progress Tracking

Enable the progress prop to receive a continuous 0–1 value representing how far the user has scrolled through the active step. Use it to drive animations tied directly to scroll position.

<Storyteller
  steps={steps}
  progress
  graphic={({ stepIndex, progress }) => (
    <div
      style={{
        opacity: progress,
        transform: `scale(${0.8 + progress * 0.2})`,
      }}
    >
      <Chart activeIndex={stepIndex} />
    </div>
  )}
/>

progress is 0 at the start of the step and 1 at the end. It is only calculated for the currently active step.

Snap Scrolling

Enable the snap prop to make steps snap into position as the user scrolls, similar to Apple's product pages. Each step locks into the viewport centre, ensuring clean transitions between steps.

<Storyteller
  steps={steps}
  snap
  graphic={({ stepIndex }) => <Chart activeIndex={stepIndex} />}
/>

snap accepts three values:

  • true or 'proximity' -- snaps when the user stops scrolling near a step boundary (recommended)
  • 'mandatory' -- always snaps to the nearest step, even mid-scroll

Under the hood this sets scroll-snap-type on <html> and scroll-snap-align: center on each step. The style is cleaned up when the component unmounts.

URL Hash Sync

Enable the hashSync prop to sync the active step to the URL hash. Users can share deep links to specific points in a story, and the page will scroll to the correct step on load.

<Storyteller
  steps={steps}
  hashSync
  graphic={({ stepIndex }) => <Chart activeIndex={stepIndex} />}
/>

As the user scrolls, the URL updates to #step-1, #step-2, etc. On page load, if the URL already contains a matching hash, the page scrolls to that step automatically.

Pass a string to customise the prefix:

<Storyteller hashSync="scene-" ... />
// URL becomes #scene-1, #scene-2, etc.

Uses history.replaceState so it doesn't pollute browser history.

Tailwind CSS

If you use Tailwind, you can replace the base stylesheet with the Tailwind plugin. It registers the same structural classes as Tailwind components and utilities.

// tailwind.config.js
import { storytellerPlugin } from '@matthesketh/react-storyteller/tailwind';

export default {
  plugins: [storytellerPlugin()],
};

Then remove the CSS import -- the plugin provides all rst-* classes via Tailwind's layer system. You can extend them with Tailwind utilities as normal:

<Storyteller
  className="max-w-7xl mx-auto"
  stepClassName="px-8 py-12"
  graphicClassName="rounded-xl overflow-hidden"
  steps={steps}
  graphic={graphic}
/>

The plugin includes:

  • Components: .rst-side-by-side, .rst-overlay, .rst-stacked, .rst-graphic--sticky, .rst-step, .rst-steps, .rst-sr-only
  • Utilities: .rst-step--snap, .rst-snap-proximity, .rst-snap-mandatory

Framer Motion

The graphic render prop works naturally with framer-motion. Wrap your graphic content in motion.div and use stepIndex as the animation key:

import { motion, AnimatePresence } from 'framer-motion';

<Storyteller
  steps={steps}
  graphic={({ stepIndex }) => (
    <AnimatePresence mode="wait">
      <motion.div
        key={stepIndex}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.4 }}
      >
        <Chart step={stepIndex} />
      </motion.div>
    </AnimatePresence>
  )}
/>

For scroll-linked animations, combine progress with motion.div style props:

<Storyteller
  progress
  steps={steps}
  graphic={({ stepIndex, progress }) => (
    <motion.div
      animate={{ scale: 0.8 + progress * 0.2, opacity: 0.5 + progress * 0.5 }}
      transition={{ type: 'tween', duration: 0 }}
    >
      <Chart step={stepIndex} />
    </motion.div>
  )}
/>

No additional dependencies are required from this package -- framer-motion is optional and only needed if you choose to use it.

Styles

Import the bundled stylesheet once at your app entry point:

import '@matthesketh/react-storyteller/styles';

The stylesheet provides:

  • Layout structure for all three modes (rst-side-by-side, rst-overlay, rst-stacked)
  • Sticky positioning for the graphic panel in side-by-side and overlay modes
  • Sensible defaults for step spacing

To override styles, target the BEM-style class names directly in your own CSS:

.rst-side-by-side {
  gap: 4rem;
}

.rst-graphic {
  background: #f5f5f5;
}

.rst-step.is-active {
  font-weight: bold;
}

Comparison

| Feature | react-storyteller | react-scrollama | @bsmnt/scrollytelling | |---|---|---|---| | React 19 | Yes | No | No | | TypeScript | Native | Separate types | Partial | | Built-in layouts | Yes (3) | No | No | | Snap scrolling | Yes | No | No | | URL hash sync | Yes | No | No | | Tailwind plugin | Yes | No | No | | Progress tracking | Yes | Yes | Yes | | Runtime deps | None | d3-selection | GSAP | | Config-driven steps | Yes | No | No |

License

MIT