Lightweight scrollytelling library using IntersectionObserver for scroll-driven interactive narratives
npx @tessl/cli install tessl/npm-scrollama@3.2.0Scrollama is a lightweight JavaScript library for creating scrollytelling (scroll-driven storytelling) experiences using the IntersectionObserver API. It provides a simple interface for detecting when elements enter or exit the viewport during scrolling, with support for step-based interactions, progress tracking, custom offsets, and sticky graphic implementations.
npm install scrollamaimport scrollama from "scrollama";For CommonJS:
const scrollama = require("scrollama");Browser (CDN):
<script src="https://unpkg.com/scrollama"></script>import scrollama from "scrollama";
// Create scrollama instance
const scroller = scrollama();
// Setup with step elements and callbacks
scroller
.setup({
step: ".step", // required: CSS selector for step elements
offset: 0.5, // trigger when element is 50% in viewport
debug: false // optional visual debugging
})
.onStepEnter((response) => {
// Fired when step enters offset threshold
const { element, index, direction } = response;
console.log("Step entered:", index, direction);
})
.onStepExit((response) => {
// Fired when step exits offset threshold
const { element, index, direction } = response;
console.log("Step exited:", index, direction);
});Scrollama uses the IntersectionObserver API for performance optimization over traditional scroll events. Key components:
scrollama() creates instances with chainable method callsCreates a new scrollama instance for managing scroll-driven interactions.
/**
* Creates a new scrollama instance
* @returns ScrollamaInstance - Instance with chainable configuration methods
*/
function scrollama(): ScrollamaInstance;Configures the scrollama instance with step elements and interaction settings.
/**
* Configure scrollama instance with steps and options
* @param options - Configuration object
* @returns ScrollamaInstance - For method chaining
*/
setup(options: ScrollamaOptions): ScrollamaInstance;
interface ScrollamaOptions {
/** Required: CSS selector, NodeList, HTMLElement[], or single HTMLElement for step elements */
step: string | NodeList | HTMLElement[] | HTMLElement;
/** Trigger position in viewport: 0-1 or string with "px" (default: 0.5) */
offset?: number | string;
/** Enable incremental progress updates (default: false) */
progress?: boolean;
/** Granularity of progress updates in pixels, minimum 1 (default: 4) */
threshold?: number;
/** Only trigger steps once, then remove listeners (default: false) */
once?: boolean;
/** Show visual debugging overlays (default: false) */
debug?: boolean;
/** Parent element for step selector, useful for shadow DOM */
parent?: HTMLElement;
/** Parent element for scroll story, for overflow: scroll/auto containers */
container?: HTMLElement;
/** Element used as viewport for visibility checking (default: browser viewport) */
root?: HTMLElement;
}Usage Example:
// Basic setup
scroller.setup({
step: ".story-step"
});
// Advanced setup with progress tracking
scroller.setup({
step: ".story-step",
offset: 0.8, // Trigger at 80% viewport height
progress: true, // Enable progress callbacks
threshold: 2, // Higher granularity
debug: true, // Show debug overlays
container: document.querySelector('.scroll-container')
});Register callbacks for step enter and exit events.
/**
* Callback fired when step elements enter the offset threshold
* @param callback - Function receiving step event data
* @returns ScrollamaInstance - For method chaining
*/
onStepEnter(callback: (response: CallbackResponse) => void): ScrollamaInstance;
/**
* Callback fired when step elements exit the offset threshold
* @param callback - Function receiving step event data
* @returns ScrollamaInstance - For method chaining
*/
onStepExit(callback: (response: CallbackResponse) => void): ScrollamaInstance;
interface CallbackResponse {
/** The DOM element that triggered the event */
element: HTMLElement;
/** Zero-based index of the step element */
index: number;
/** Scroll direction when event fired */
direction: "up" | "down";
}Usage Example:
scroller
.onStepEnter((response) => {
const { element, index, direction } = response;
element.classList.add('is-active');
// Handle different steps
if (index === 0) {
// First step logic
} else if (index === 2) {
// Third step logic
}
})
.onStepExit((response) => {
const { element, index, direction } = response;
element.classList.remove('is-active');
});Monitor fine-grained progress within step elements (requires progress: true in setup).
/**
* Callback fired for incremental progress through steps
* @param callback - Function receiving progress event data
* @returns ScrollamaInstance - For method chaining
*/
onStepProgress(callback: (response: ProgressCallbackResponse) => void): ScrollamaInstance;
interface ProgressCallbackResponse {
/** The DOM element being tracked */
element: HTMLElement;
/** Zero-based index of the step element */
index: number;
/** Progress through the step (0.0 to 1.0) */
progress: number;
/** Current scroll direction */
direction: "up" | "down";
}Usage Example:
scroller
.setup({
step: ".step",
progress: true,
threshold: 1 // Very granular updates
})
.onStepProgress((response) => {
const { element, index, progress, direction } = response;
// Update progress bar
const progressBar = element.querySelector('.progress-bar');
progressBar.style.width = `${progress * 100}%`;
// Fade in/out based on progress
element.style.opacity = progress;
});Get or set the trigger offset position dynamically.
/**
* Get or set the offset trigger value (note: method name is 'offset' in implementation)
* @param value - Optional new offset value (0-1 or string with "px")
* @returns ScrollamaInstance if setting, current offset value if getting
*/
offset(value?: number | string): ScrollamaInstance | number;Usage Example:
// Get current offset
const currentOffset = scroller.offset();
// Set new offset
scroller.offset(0.25); // Trigger at 25% viewport height
scroller.offset("100px"); // Trigger at 100px from topControl scrollama instance state and lifecycle.
/**
* Resume observing for trigger changes (if previously disabled)
* @returns ScrollamaInstance - For method chaining
*/
enable(): ScrollamaInstance;
/**
* Stop observing for trigger changes
* @returns ScrollamaInstance - For method chaining
*/
disable(): ScrollamaInstance;
/**
* Manual resize trigger (automatic with built-in ResizeObserver)
* @returns ScrollamaInstance - For method chaining
*/
resize(): ScrollamaInstance;
/**
* Remove all observers and callback functions
* @returns void
*/
destroy(): void;Usage Example:
// Temporarily disable
scroller.disable();
// Re-enable later
scroller.enable();
// Clean up when done
scroller.destroy();Override the global offset for individual step elements using data attributes.
<!-- Use percentage (0-1) -->
<div class="step" data-offset="0.25">Step with 25% offset</div>
<!-- Use pixels -->
<div class="step" data-offset="100px">Step with 100px offset</div>
<!-- Use default global offset -->
<div class="step">Step with global offset</div>Complete TypeScript type definitions for all interfaces and callbacks.
interface ScrollamaInstance {
setup(options: ScrollamaOptions): ScrollamaInstance;
onStepEnter(callback: StepCallback): ScrollamaInstance;
onStepExit(callback: StepCallback): ScrollamaInstance;
onStepProgress(callback: StepProgressCallback): ScrollamaInstance;
offset(value?: number | string): ScrollamaInstance | number;
resize(): ScrollamaInstance;
enable(): ScrollamaInstance;
disable(): ScrollamaInstance;
destroy(): void;
}
type DecimalType = 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1;
type StepCallback = (response: CallbackResponse) => void;
type StepProgressCallback = (response: ProgressCallbackResponse) => void;<div class="container">
<div class="graphic">
<!-- Fixed graphic content -->
</div>
<div class="scroller">
<div class="step" data-step="1">First step content</div>
<div class="step" data-step="2">Second step content</div>
<div class="step" data-step="3">Third step content</div>
</div>
</div>.container {
display: flex;
}
.graphic {
position: sticky;
top: 0;
flex: 1;
height: 100vh;
}
.scroller {
flex: 1;
}
.step {
margin-bottom: 80vh;
padding: 1rem;
}// Use pixel offsets for consistent mobile behavior
const isMobile = window.innerWidth < 768;
const offset = isMobile ? "150px" : 0.5;
scroller.setup({
step: ".step",
offset: offset
});// Add new steps dynamically
function addStep(content) {
const newStep = document.createElement('div');
newStep.className = 'step';
newStep.textContent = content;
document.querySelector('.scroller').appendChild(newStep);
// Reconfigure scrollama
scroller.setup({
step: ".step" // Automatically picks up new steps
});
}