ScrollMagic
The javascript library for magical scroll interactions.
Install / Use
/learn @janpaepke/ScrollMagicREADME
ScrollMagic 3
<!-- TODO: Replace static shields (license, bundle, dependencies) once published  -->The lightweight library for magical scroll interactions
Looking for ScrollMagic v2? The legacy version is on the
v2-stablebranch.
ScrollMagic tells you where an element is relative to the viewport as the user scrolls — and fires events when that changes.
It's a convenience wrapper around IntersectionObserver and ResizeObserver that handles the performance pitfalls and counter-intuitive edge cases for you.
Not an animation library – unless you want it to be
By itself, ScrollMagic doesn't animate anything. It provides precise scroll-position data and events — what you do with them is up to you. If you're looking for a ready-made scroll animation solution, check out GSAP ScrollTrigger, Motion, or anime.js.
For pure CSS-driven scroll animations, see native scroll-driven animations (not yet supported in all browsers). ScrollMagic complements them by providing cross-browser support, event callbacks, progress values, and state management that the native API doesn't cover.
ScrollMagic is a general-purpose, framework-agnostic, zero-dependency foundation for scroll-driven UX — what you do with it is entirely up to you: class toggles, animations, lazy loading, parallax, scroll-linked video, behavioural tracking, or anything else.
Why ScrollMagic?
- Tiny footprint, zero dependencies
- Free to use (open source)
- Optimized for performance (shared observers, batched rAF, single-frame updates)
- Built for modern browsers, mobile compatible
- Native TypeScript support
- SSR safe
- Works with any scroll container (window or custom element)
- Horizontal and vertical scrolling
- Plugin system for extensibility
- Framework agnostic — works with React, Vue, vanilla JS, anything
Installation
npm install scrollmagic@next
Quick Start
import ScrollMagic from 'scrollmagic';
new ScrollMagic({ element: '#my-element' })
.on('enter', () => console.log('visible!'))
.on('leave', () => console.log('gone!'))
.on('progress', e => console.log(`${(e.target.progress * 100).toFixed(0)}%`));
How It Works
ScrollMagic uses two sets of bounds to define the active range:
- Container bounds — a zone on the scroll container, defined by
containerStartandcontainerEnd - Element bounds — a zone on the tracked element, defined by
elementStartandelementEnd
Progress goes from 0 to 1 as the element bounds pass through the container bounds. Events fire on enter, leave, and progress change.
Contain and Intersect
The two most common configurations are contain and intersect. They differ in where the container bounds are positioned:
Contain (default when element is null)
<img align="right" src="docs/dist/gfx/contain.gif" alt="Contain mode animation: tall element scrolls through viewport, progress tracks from 0% to 100%" width="260" />
The container bounds match the viewport edges — containerStart and containerEnd are both at 'here' (0%). Progress goes from 0 to 1 while one fully contains the other: either the element is fully visible inside the viewport, or the element fully covers the viewport.
Typical uses: scroll progress bars, parallax, scroll-linked video, scroll-driven storytelling.
<br clear="both" />Intersect (default when element is set)
<img align="right" src="docs/dist/gfx/intersect.gif" alt="Intersect mode animation: element scrolls through the viewport, progress tracks from 0% to 100%" width="260" />
The container bounds span the full viewport — containerStart and containerEnd are at 'opposite' edges (100%). Progress goes from 0 to 1 while the element intersects with the viewport: starting when its leading edge enters and ending when its trailing edge leaves.
Typical uses: enter/leave animations, lazy loading, class toggles, visibility tracking.
<br clear="both" />Not just defaults
While contain and intersect are the inferred defaults, you can also configure them explicitly — for example setting containerStart: 0, containerEnd: 0 on an instance that has an element to get contain behaviour, or mixing container and element insets for custom tracking zones. The two configurations are useful mental models, not rigid modes.
Native scroll-driven animation ranges
If you're familiar with CSS scroll-driven animations, here's how the native view() timeline ranges map to ScrollMagic configurations:
| Native range | ScrollMagic equivalent |
| ------------ | ---------------------- |
| cover | intersect default — containerStart: 'opposite', containerEnd: 'opposite' |
| contain | contain default — containerStart: 0, containerEnd: 0 |
| entry | containerStart: 'opposite', containerEnd: 0 — container zone collapses to the trailing edge |
| exit | containerStart: 0, containerEnd: 'opposite' — container zone collapses to the leading edge |
The native entry-crossing and exit-crossing ranges are equivalent to entry and exit above — the distinction only applies when subdividing a single native timeline, not when defining standalone tracking ranges.
Options
All options are optional. They can be passed to the constructor and updated at any time via setters or .modify().
| Option | Type | Default | Description |
| ---------------- | -------------------------------------- | -------------------------- | ----------------------------------------------------- |
| element | Element \| string \| null | first child of container | The tracked element (or CSS selector). Selectors match only the first element — create one instance per element to track multiple. |
| elementStart | number \| string \| function | 0 | Start inset on the element. |
| elementEnd | number \| string \| function | 0 | End inset on the element. |
| container | Window \| Element \| string \| null | window | The scroll container (or CSS selector). Selectors use the first match. |
| containerStart | number \| string \| function \| null | inferred (see below) | Start inset on the scroll container. |
| containerEnd | number \| string \| function \| null | inferred (see below) | End inset on the scroll container. |
| vertical | boolean | true | Scroll axis. true = vertical, false = horizontal. |
Inset values work like CSS top/bottom: positive values offset inward from the respective edge in the tracked direction. Accepted value types:
- Numbers — pixel values (e.g.
50) - Strings — percentage or pixel strings (e.g.
'50%','20px'), relative to the parent size (scroll container for container options, element for element options) - Named positions —
'here'(0%),'center'(50%),'opposite'(100%) - Functions —
(size) => numberfor dynamic computation
null means infer: For element, container, containerStart, or containerEnd, setting it to null resets them to their inferred default.
For containerStart/containerEnd the inferred values depend on element:
elementisnull→ defaults to contain: the element is inferred as the first child of the container (forwindowthis isdocument.body), container offsets are'here'(0%), mapping progress to overall scroll position.elementis notnull→ defaults to intersect: container offsets are'opposite'(100%), tracking the element as it scrolls through the full viewport.
Events
Subscribe with .on(), .off(), or .subscribe() (returns an unsubscribe function). Pass { once: true } to auto-remove the listener after its first invocation. Calling .off() or the unsubscribe function after the listener has already been removed (e.g. after a once listener fires) is a safe no-op.
| Event | When |
| ---------- | -------------------------------------------------------- |
| enter | Element enters the active zone (progress leaves 0 or 1) |
| leave | Element leaves the active zone (progress reaches 0 or 1) |
| progress | Progress value changes while in the active zone |
Every event provides:
event.target; // the ScrollMagic instance (a
