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

@fundar/data-scrolly-telling

v0.0.30

Published

A library for building scrollytelling experiences in Svelte in a declarative way.

Downloads

434

Readme

data-scrolly-telling

A library for building scrollytelling experiences in Svelte in a declarative way.


📑 Table of Contents


🔧 Requirements

  • Node.js >= 18
  • pnpm >= 8

Install pnpm if needed:

npm install -g pnpm

🚀 Setup

Clone the repo and install dependencies:

# Preparation
pnpm install

# Development and Git workflow
git status
git add .
git commit -m "..."

# NPM workflow
npm login
npm version patch
npm publish --access public

🎯 Core Concepts

At a high level, a scrollytelling component in this library is composed of:

  • A Scrolly container that orchestrates everything
  • A layout (e.g. SideBySide) that defines the spatial structure
  • Two main content streams:
    • Data → visual layers (Layer)
    • Telling → narrative steps (Step)

🔭 Basic Example

<script lang="ts">
  import { Scrolly, SideBySide, Layer, Step } from '@fundar/data-scrolly-telling';
  import { threshold } from '$lib/stores/threshold';
</script>

{#snippet data()}
  <Layer fill>...</Layer>
{/snippet}

{#snippet telling()}
  <Step threshold={$threshold}>...</Step>
  <Step threshold={$threshold}>...</Step>
  <Step threshold={$threshold}>...</Step>
{/snippet}

<Scrolly
  {data}
  {telling}
  layout={SideBySide}
  layoutProps={{
    inverted: false,
    gridColumns: '1fr 3fr',
    offsetTop: '64px',
    offsetBottom: '0px'
  }}
  cssClass="example-steps"
/>

<style>
  :global(.example-steps) {
    ...
  }
</style>

🧠 Mental Model


Scrolly (Orchestrator)

The Scrolly component is the entry point and main orchestrator.

It is responsible for:

  • Synchronizing scroll position with the active step
  • Connecting telling (Steps) with data (Layers)
  • Delegating rendering to a layout component
  • Managing global configuration (offsets, CSS hooks, etc.)

Layout

A layout defines how the data and telling streams are arranged on screen.

In our example, SideBySide renders:

  • Narrative content (Step) on one side
  • Visual content (Layer) on the other

Layouts are:

  • Pluggable
  • Configurable via layoutProps

data snippet → Layer

The data snippet defines the visual stack.

  • Composed of one or more Layer components
  • Layers are typically:
    • Backgrounds
    • Images
    • Videos
    • Charts
    • Maps
    • Any visual element that reacts to scroll

Think of it as a scene graph that persists across steps.


telling snippet → Step

The telling snippet defines the narrative flow.

  • Composed of multiple Step components
  • Each Step represents a scroll-triggered state
  • Steps can:
    • Activate/deactivate layers
    • Trigger transitions
    • Drive animations via thresholds

This is one way to model your narrative timeline.


⚙️ Scrolly

The Scrolly component is the core orchestrator of the system. It connects scroll position with rendering, coordinates the layout, and exposes a timeline API for reacting to scroll progress.

🔍 Example usage

<Scrolly
  {data}
  {telling}
  layout={SideBySide}
  layoutProps={{
    inverted: false,
    gridColumns: '1fr 3fr',
    offsetTop: '64px',
    offsetBottom: '0px'
  }}
  cssClass="example-steps"
/>

📦 Props

type ScrollyProps = {
  layout: Component<any>;
  layoutProps?: Record<string, any>;
  telling: Snippet;
  data?: Snippet;
  timeline?: TimelineConfig;
  cssClass?: string;
};

layout

A Svelte component responsible for rendering the overall structure.

  • Receives telling and data snippets
  • Receives an internal onTellingMount callback (used to measure scrollable area)
  • May optionally implement setProgress(progress: number) to react to scroll

layoutProps

An object passed directly to the layout component.

Use this to configure layout-specific behavior (e.g. grid, offsets, orientation).

telling

A required snippet that defines the narrative flow.

  • Typically composed of Step components
  • Mounted inside the layout
  • Used as the reference element to compute scroll progress

data

An optional snippet that defines the visual layer stack.

  • Typically composed of Layer components
  • Rendered alongside telling depending on the layout

cssClass

Optional class applied to the layout container and its layers and steps.

  • Useful for scoping styles
  • Typically used with :global(...) when styling from outside

[!NOTE] Details about available css variables are covered here.

timeline

An optional, low-level API for reacting to scroll progress.

While Step provides a declarative way to structure narrative state, timeline allows imperative control over scroll-driven behavior.

type TimelineConfig = TimelineEntry[];

type TimelineEntry =
  | { at: number; callback: (progress: number) => void; once?: boolean }
  | { on: 'progress'; callback: (progress: number) => void }
  | { on: 'intersection'; callback: (progress: number) => void }
  | { from: number; to: number; callback: (progress: number) => void };

Entry type 1: Trigger (at)

Executes a callback when scroll progress crosses a specific point.

{
  at: 0.5,
  callback: (progress) => { ... },
  once: true // default: true
}
  • Fires when crossing the threshold
  • Supports forward/backward detection
  • Can be configured to fire only once

Entry type 2: Interval (from / to)

Executes continuously while progress is within a range.

{
  from: 0.2,
  to: 0.6,
  callback: (localProgress) => { ... }
}
  • localProgress is normalized between 0 → 1 within the interval
  • Useful for animations tied to a segment of the scroll

Entry type 3: Progress (on: 'progress')

Executes on every scroll update while measuring how much of the scrolly container is visible in the viewport.

{
  on: 'progress',
  callback: (progress) => { ... }
}
  • Receives viewport intersection ratio (0 → 1)
  • 0 = component completely outside viewport
  • 1 = component fully occupying the viewport (or as much as possible)
  • Useful for early entrance animations and viewport-driven effects

Entry type 4: Intersection (on: 'intersection')

Executes on every scroll update.

{
  on: 'intersection',
  callback: (intersection) => { ... }
}
  • Receives global progress (0 → 1)
  • Lowest-level hook

Scroll Model:

Scrolly computes a normalized scroll progress (0 → 1) based on:

  • The bounding box of the telling content
  • Viewport height
  • Configurable top/bottom offsets (provided by the layout)

This progress value is:

  • Propagated to the timeline
  • Forwarded to the layout via setProgress

🗒️ Notes

  • Scrolly does not impose a specific storytelling model
  • You can combine:
    • Declarative Step-based flows
    • Imperative timeline logic
  • Layouts remain fully decoupled from scroll computation

🎨 Layer

A Layer represents a visual element within the data stream.

It is the fundamental building block for rendering visuals (charts, images, maps, etc.) that persist and react across scroll.

It is rendered as an absolutely positioned element and participates in a shared visual stack (multiple layers can overlap and be composed to build complex scenes)

🔍 Example usage

Layer components must be defined inside the data snippet.

{#snippet data()}
  <Layer fill>
    ...
  </Layer>
{/snippet}

Internally, Layer relies on a DataScope context provided by Scrolly. Using it outside this scope will result in an error.


📦 Props

type LayerProps = {
  transition?: TransitionConfig;
  position?: PositionConfig;
  cssClasses?: string;
  fill?: boolean;
  children?: Snippet;
};

children

Content rendered inside the layer.

  • Can be any Svelte markup
  • Typically visual components (SVG, canvas, images, etc.)

fill

fill?: boolean;

If true, the layer expands to fill its container.

  • Applies position: absolute with full inset
  • Commonly used for background layers

cssClasses

cssClasses?: string;

Additional CSS classes applied to the root element.

  • Useful for styling or targeting specific layers

position

position?: PositionConfig;

Defines the static visual state of the layer.

type PositionConfig = {
  x?: string | number;
  y?: string | number;
  scale?: number;
  rotate?: string | number;
  opacity?: number;
  blur?: string;
};
  • Applied immediately on mount
  • Typically used as a base state before transitions

transition

transition?: TransitionConfig;

Defines how the layer animates.

type TransitionConfig = {
  preset?: TransitionPreset;
  opacity?: [number, number];
  translateX?: [string, string];
  translateY?: [string, string];
  scale?: [number, number];
  rotate?: [string, string];
  blur?: [string, string];
  duration?: string;
  easing?: EasingFunction;
};
  • Applied via an internal animation adapter
  • Enables interpolation between states
  • Often driven indirectly by Step or timeline logic

[!NOTE] Details about animation presets and interpolation are covered in a later section.

[!WARNING] Do not use Layer inside the telling snippet. Layers are not inherently tied to steps; they are global within the scrolly instance.


💬 Step

A Step represents a unit of narrative in the telling stream.

It defines when something happens during scroll and provides hooks (callbacks) to react to those moments.

🔍 Example usage

Step components must be defined inside the telling snippet.

{#snippet telling()}
  <Step>
    <p>First section</p>
  </Step>

  <Step>
    <p>Second section</p>
  </Step>
{/snippet}

Internally, Step registers itself in the TellingScope, where it is observed and coordinated.


📦 Props

type StepProps = {
  threshold?: ThresholdConfig;
  symmetric?: SymmetricMode;

  onEnter?: (entry: IntersectionObserverEntry) => void;
  onExit?: (entry: IntersectionObserverEntry) => void;
  onComplete?: (entry: IntersectionObserverEntry) => void;
  onInactive?: (entry: IntersectionObserverEntry) => void;
  onReturn?: (entry: IntersectionObserverEntry) => void;

  onIntersection?: (ratio: number, entry: IntersectionObserverEntry) => void;
  onProgress?: (progress: number, entry: IntersectionObserverEntry) => void;

  showData?: boolean | 'force-refresh';
  dvhHeightFactor?: number;
  transition?: SlideTransition | null;

  layout?: StepLayout;
  container?: StepContainer;
  cssClasses?: string;

  children?: Snippet;
};

threshold

type ThresholdConfig =
  | { type: 'percentage'; value: number }
  | { type: 'px'; value: number }
  | { type: 'rootMargin'; value: string };

Defines when the step becomes active (the active region).

  • percentage → relative to viewport (e.g. 50 → 50% = middle)
  • px → fixed offset
  • rootMargin → full control using IntersectionObserver semantics

This is the primary mechanism for aligning narrative timing with scroll.

Callbacks

Steps expose a set of lifecycle and continuous callbacks.

Discrete events
  • onEnter → step crosses threshold entering into active region
  • onExit → leaves threshold region
  • onComplete → entire step crosses threshold
  • onInactive → entire step moves above viewport
  • onReturn → re-enters from above (reverse scroll)
Continuous events
  • onIntersection(ratio) → raw intersection ratio between [0, 1]. Percentage of the step which is visible and its inside the active region.
  • onProgress(progress) → normalized progress within the step. This should be 0 when fires onEnter and 1 when fires onInactive.

symmetric

type SymmetricMode = 'onEnter' | 'onInactive' | 'both';

Controls how callbacks behave when scrolling backwards.

  • 'onEnter' → mirror enter behavior of (n)-step with exit behavior of (n+1)-step.
  • 'onInactive' → mirror inactive behavior (n)-step with return behavior of (n+1)-step.
  • 'both' → fully symmetric on enter and inactive behavior.

This is useful for reversible narratives with discrete events.

layout

type StepLayout =
  | { type: 'inline' }
  | { type: 'centered' }
  | { type: 'columns'; columns: number; gap?: string };

Defines how content inside the step is arranged.

  • inline → default block flow
  • centered → vertically + horizontally centered (Useful for Slide layout).
  • columns → grid layout (Useful for Slide layout).

container

type StepContainer = {
  maxWidth?: string;
  padding?: string;
};

Controls the inner content width.

  • Applied as a wrapper inside the step
  • Useful for readable text layouts

[!NOTE] These props only apply to the Slide layout.

showData

showData?: boolean | 'force-refresh';

Controls whether the shared data snippet is rendered for the current step.

  • true → mounts the shared data layer for this step.
  • 'force-refresh' → same as true, but also forces a resize/layout refresh after mounting.

'force-refresh' is typically useful for the first step displaying data, ensuring responsive charts and layouts recompute correctly after being portaled into view.

dvhHeightFactor

dvhHeightFactor?: number;

Multiplier applied to the layout-level dvhHeightPerStep.

This allows individual steps to occupy more or less virtual scroll height than the default.

Examples:

  • 1 → default height
  • 2 → step lasts twice as long
  • 0.5 → shorter step

This exists primarily for the Slide layout, where sticky slide behavior can make scroll pacing feel less intuitive without fine-grained control.

transition

type SlideTransition = {
  enter?: TransitionConfig | null;
  exit?: TransitionConfig | null;
};

transition?: SlideTransition | null;

Overrides the default enter/exit transitions defined by the Slide layout.

  • enter → transition applied when the step becomes active
  • exit → transition applied when the step leaves
  • null → disables that transition completely

Useful for customizing step-level motion behavior independently from the layout defaults.


🎨 Styling

Steps expose a large set of CSS custom properties for styling.

Example:

:global([data-telling-step]) {
  --step-padding-y: 4rem;
  --step-max-width: 800px;
  --step-background: white;
  --step-border-radius: 8px;
}

Available categories

  • Spacing--step-padding-*, --step-margin-*
  • Typography--step-font-size, --step-line-height, etc.
  • Background--step-background, --step-background-color
  • Border--step-border-*, --step-border-radius
  • Layout--step-width, --step-height
  • Effects--step-opacity, --step-filter, --step-box-shadow

All styles are applied to the root <section> element.

You can define custom CSS classes that set these variables and apply them in two ways:

  • Globally, by passing a class to Scrolly via cssClass (useful for scoping styles across all steps)
  • Per step, by passing a class via the cssClasses prop on Step.

[!TIP] If you are using TailwindCSS, you can use its classes too.

<script lang="ts">
  import { Layer, Scrolly, Step, Overlay } from '@fundar/data-scrolly-telling';
</script>

{#snippet data()}
  ...
{/snippet}

{#snippet telling()}
  ...
  <Step cssClasses="highlighted-step">  // specific custom css class for this step
    ...
  </Step>
  ...
  <Step cssClasses="bg-blue-500">  // specific tailwind class for this step
    ...
  </Step>
  ...
  <Step cssClasses="highlighted-step my-5">  // or both...
    ...
  </Step>
  ...
{/snippet}

<Scrolly
  {data}
  {telling}
  timeline={[]}
  layout={Overlay}
  layoutProps={{
    offsetTop: '64px',
    offsetBottom: '0px'
  }}
  cssClass="overlay-steps"  // general styling for all steps
/>

<style>
  :global(.overlay-steps) {
    --step-padding: 1rem;  // you can set general step's css variables
    --step-color: white;  // you can use normal css for this
    --step-background-color: var(--color-base-300);  // or you can use TailwindCSS variables too
    --step-border-radius: 8px;
    --step-max-width: 600px;
  }

  :global(.highlighted-step) {
    background-color: gold;
  }
</style>

🧠 State Model

Each Step is internally modeled as a state machine.

States

  • idle → below threshold, not yet active
  • intersect → intersecting the threshold (active)
  • completed → entire step is above threshold and passed completion point (active)
  • inactive → entire step is above viewport

Transitions

  • idle → intersect
  • intersect → completed
  • intersect → idle
  • intersect → inactive
  • completed → intersect
  • completed → inactive
  • inactive → intersect

FSM

stateDiagram-v2
  [*] --> idle

  idle --> intersect : onEnter

  intersect --> completed : onComplete
  intersect --> idle : onExit
  intersect --> inactive : onInactive

  completed --> intersect : 
  completed --> inactive : onInactive

  inactive --> intersect : onReturn

[!NOTE] Steps are observed and registered automatically via context.

[!TIP]

  1. Multiple callbacks can be combined within a single step.
  2. Prefer Step for narrative structure, and Scrolly's timeline for low-level control.

[!WARNING] Do not use Step inside the data snippet.


🧱 Layouts

A layout defines how data (Layer) and telling (Step) are arranged and rendered on screen.

While Scrolly handles scroll logic and synchronization, the layout is responsible for:

  • Structuring the DOM
  • Positioning data and telling
  • Defining how visuals behave during scroll (e.g. sticky, overlay, transitions)
  • Providing offsets used to compute scroll progress

Layouts are:

  • Pluggable → passed via the layout prop in Scrolly
  • Configurable → via layoutProps
  • Reactive to scroll progress (optional) → layouts can implement setProgress(progress) to respond to the Scrolly engine and drive custom behaviors (e.g. transitions, slide-based narratives)

📦 All layouts receive the following props:

  • telling snippet
  • data snippet (optional)
  • offsetTop (default: '0px')
  • offsetBottom (default: '0px')
  • onTellingMount (callback used by Scrolly)

[!NOTE] Layouts define how scroll space is computed via offsetTop / offsetBottom

[!TIP] Some layouts (like Slide) implement custom behavior via setProgress


🧭 Available Layouts


📚 Overlay

Renders data and telling in the same spatial context, layered on top of each other.

  • data is sticky and remains fixed within the viewport
  • telling scrolls normally on top
  • Both layers can overlap using z-index

Use case:

  • Background visualizations with text scrolling above
  • Classic scrollytelling pattern (map/chart + narrative)

📦 Specific props:

  • zIndexData (default: 0)
  • zIndexTelling (default: 1)

📐 SideBySide

Renders data and telling as two columns in a grid.

  • data is sticky
  • telling scrolls vertically
  • Layout is controlled via CSS grid

Use case:

  • Analytical layouts
  • Text + visualization side-by-side

📦 Specific props:

  • zIndexData (default: 0)
  • zIndexTelling (default: 1)
  • inverted (default: false - telling to the left and data to the right)
  • gridColumns (default: 1fr 1fr - half of the viewport for each snippet)

🎞️ Slide

Transforms the narrative into a sequence of full-screen slides.

  • Steps are stacked and transitioned in/out
  • Scroll is mapped to discrete slide changes
  • Sticky viewport-based storytelling

Use case:

  • Presentation-style storytelling
  • Full-screen immersive narratives
  • Data-driven slide sequences

📦 Specific props:

  • transition
transition?: SlideTransition;

Defines the default transitions used by all steps inside the layout.

type SlideTransition = {
  enter?: TransitionConfig | null;
  exit?: TransitionConfig | null;
};

Both enter and exit transitions can be configured independently.

Example:

transition={{
  enter: { preset: 'slide-up' },
  exit: { preset: 'fade' }
}}
dvhHeightPerStep
dvhHeightPerStep?: number;

Defines the default virtual scroll height assigned to each step.

This controls the overall pacing and total scroll length of the slide sequence.

Larger values make each slide remain active longer during scrolling.

[!TIP] Individual Steps can override both transitions and virtual height using its own props:

  • transition
  • dvhHeightFactor

🎬 Transition system:

Transitions are built from composable animation properties.

A transition may use a predefined preset:

type TransitionPreset =
  | 'fade'
  | 'slide-up'
  | 'slide-down'
  | 'slide-left'
  | 'slide-right'
  | 'scale-in'
  | 'scale-out'
  | 'rotate'
  | 'flip-x'
  | 'flip-y'
  | 'blur';

Or fully custom animation values:

type TransitionConfig = {
  preset?: TransitionPreset;
  opacity?: [number, number];
  translateX?: [string, string];
  translateY?: [string, string];
  scale?: [number, number];
  rotate?: [string, string];
  blur?: [string, string];
  duration?: string;
  easing?: EasingFunction;
};

This allows combining presets with low-level overrides for fine-grained motion control.


🧩 Patterns

🌄 Parallax

Parallax is not a dedicated layout, but a composition pattern built on top of existing primitives.

It is typically implemented using:

  • Overlay layout (for stacking layers)
  • Multiple Layers (representing depth planes)
  • A timeline (or Step callbacks) to drive motion

Concept

Parallax works by moving elements at different speeds relative to scroll progress, creating a sense of depth.

  • Background elements → move slowly
  • Foreground elements → move faster
  • Some elements may remain fixed

Guidelines

  1. Use Overlay as the base layout to stack visual layers
  2. Define each visual element as a Layer
  3. Bind scroll progress via timeline
  4. Map progress (0 → 1) to visual properties (e.g. y position, rotate, scale)

[!NOTE] There is no fixed structure: the effect emerges from how layers are orchestrated.

Example

  1. Import the required components:
<script lang="ts">
  import { Layer, Scrolly, Step, Overlay } from '@fundar/data-scrolly-telling';
</script>
  1. Define your data snippet using multiple Layers.

In this example, we create a simple space scene:

  • A background of randomly generated stars
  • Two visual layers (Moon and Earth) whose positions depend on scroll progress
{#snippet data()}
  <svg class="stars" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice">
    {#each stars as s}
      <circle cx={s.x} cy={s.y} r={s.r} fill="white" opacity={s.opacity} />
    {/each}
  </svg>

  <Layer cssClasses="moon" position={{ y: `${moonY}vh` }}>
    <img src="/img/moon.png"/>
  </Layer>

  <Layer cssClasses="earth" position={{ y: `${earthY}vh` }}>
    <img src="/img/earth.png"/>
  </Layer>
{/snippet}

<style>
  .stars {
    position: absolute;
    background-color: black;
    inset: 0;
    width: 100%;
    height: 100%;
  }
</style>
  1. Derive layer positions from a reactive currentProgress value.

Each layer moves at a different rate, creating the parallax effect.

<script lang="ts">
  ...
  let currentProgress = $state(0);

  const moonY = $derived(50 - currentProgress * 20);
  const earthY = $derived(30 - currentProgress * 120);
</script>
  1. Define the telling snippet with Steps.
{#snippet telling()}
  <Step threshold={$threshold}>...</Step>
  <Step threshold={$threshold}>...</Step>
  <Step threshold={$threshold}>...</Step>
{/snippet}
  1. Connect scroll progress of Scrolly to your reactive state.
<Scrolly
  {data}
  {telling}
  timeline={[{ on: 'progress', callback: (p) => currentProgress = p }]}
  layout={Overlay}
/>