ScrollMagic README

The project's README from github.com/janpaepke/ScrollMagic. As one markdown file: README.md.

ScrollMagic 3

npm version license bundle size dependencies TypeScript

The lightweight library for magical scroll interactions

Looking for ScrollMagic v2? The legacy version is on the v2-stable branch.

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.

Donate

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?

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:

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)

Contain mode animation: tall element scrolls through viewport, progress tracks from 0% to 100%

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.


Intersect (default when element is set)

Intersect mode animation: element scrolls through the viewport, progress tracks from 0% to 100%

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.


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:

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:

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 (access all properties, e.g. event.target.progress, event.target.element)
event.type; // 'enter' | 'leave' | 'progress'
event.direction; // 'forward' | 'reverse'
event.location; // 'start' | 'inside' | 'end'

Examples

// Intersect (default): active while any part of the element
// is visible in the viewport
new ScrollMagic({
	element: '#a',
});

// Intersect with narrowed container zone:
// active while the element passes through the center line
new ScrollMagic({
	element: '#b',
	containerStart: 'center',
	containerEnd: 'center',
});

// Same as above, but with element offsets:
// starts 50px before the element, ends 100px after it
new ScrollMagic({
	element: '#c',
	containerStart: 'center',
	containerEnd: 'center',
	elementStart: -50,
	elementEnd: -100,
});

// Fixed scroll distance of 150px, regardless of element height.
// elementEnd receives the element's size and offsets from
// the bottom — (size - 150) leaves only 150px of track.
new ScrollMagic({
	element: '#d',
	containerStart: 'center',
	containerEnd: 'center',
	elementEnd: size => size - 150,
});

// Contain: active only while the element is fully visible
// (element insets pushed to opposite edges = full element height)
new ScrollMagic({
	element: '#e',
	elementStart: 'opposite', // same as '100%'
	elementEnd: 'opposite', // same as '100%'
});

// Contain (default when no element): track overall scroll progress
new ScrollMagic();

API

const sm = new ScrollMagic(options);

// Event listeners
sm.on(type, callback); // add listener, returns instance (chainable)
sm.on(type, callback, { once: true }); // listener auto-removes after first invocation
sm.off(type, callback); // remove listener, returns instance (chainable)
sm.subscribe(type, callback); // add listener, returns unsubscribe function
sm.subscribe(type, callback, { once: true }); // both auto-removes and returns unsubscribe

// Modify options after creation
sm.modify({ containerStart: 'center' });

// All options can also be directly read and written
const elem = sm.element; // get the tracked element
sm.containerStart = 'center'; // set individual options

// Read-only getters
sm.progress; // 0–1, how far through the active zone
sm.activeRange; // { start, end } container scroll positions where tracking is active
sm.scrollVelocity; // px/s along tracked axis, 0 when idle
sm.resolvedBounds; // { element, container } cached layout bounds

// Refresh — recalculate bounds after external layout changes
sm.refresh();

// Pause / resume tracking without destroying
sm.disable(); // disconnects all observers, freezes progress
sm.enable(); // reconnects observers, recalculates from current state
sm.disabled; // read-only, true when disabled or destroyed

// Lifecycle
sm.destroy();

// Static
ScrollMagic.defaultOptions({ vertical: false }); // get/set defaults for new instances
ScrollMagic.refreshAll(); // refresh every active instance
ScrollMagic.destroyAll(); // destroy every active instance

When to use refresh()

ScrollMagic automatically tracks element size changes (via ResizeObserver) and scroll position changes. But some layout changes are invisible to these observers — they change an element's position without changing its size or triggering a scroll event.

Call refresh() (or ScrollMagic.refreshAll()) after:

// After changing a style that affects position
element.style.marginTop = '100px';
sm.refresh();

// After fonts finish loading (affects text reflow)
document.fonts.ready.then(() => ScrollMagic.refreshAll());

// After a framework re-render that changes layout
onRouteChange(() => ScrollMagic.refreshAll());

Note that refresh() is only needed if you want bounds to update before the next scroll event. If the user keeps scrolling, element positions are re-read on every scroll frame anyway. refresh() matters when layout changes while tracking is active and the scroll position stays the same — e.g. toggling a class or injecting content without any scrolling.

refresh() is asynchronous — it schedules recalculation for the next animation frame and returns immediately. Multiple refresh() calls within the same frame are batched automatically.

Plugins

ScrollMagic has a plugin system for extending instance behaviour.

sm.addPlugin(myPlugin);
sm.removePlugin(myPlugin);

See PLUGINS.md for the full plugin authoring guide.

Browser Support

Chrome 73+, Firefox 69+, Safari 13.1+, Edge 79+ (aligned to ResizeObserver support).

License

MIT — Jan Paepke

ScrollMagic (janpaepke/ScrollMagic)

Searching Database…

这是什么?ScrollMagic 是一个轻量级、与框架无关的库。用户滚动页面时,它会跟踪元素相对于视口的位置,并触发进入、离开和进度事件,供你接入使用。
这是像 GSAP 那样的动画库吗?不是。它只提供滚动位置数据和事件,本身不会移动或设置元素的样式。要实现真正的动画,请搭配 GSAP ScrollTrigger、Motion、anime.js,或你自己的 CSS/JS 使用。
我该如何把它添加到项目中?安装该包,然后创建 `new ScrollMagic({ element, container, ... })` 实例,再用 .on() 或 .subscribe() 订阅其进入、离开和进度事件。具体代码示例见 README 中的 “Installation” 和 “Quick Start” 章节。
它需要其他框架或库才能运行吗?不需要。它是零依赖、与框架无关的基础库,无论你使用 React、Vue、原生 JS,还是完全不用框架,都能同样良好地运行,前提是浏览器支持 ResizeObserver。
页面布局变化时,我需要手动调用什么吗?需要。ScrollMagic 会通过 ResizeObserver 自动跟踪尺寸和滚动变化,但以下情况需要手动调用 refresh() 或 ScrollMagic.refreshAll():不改变元素尺寸、也不触发滚动事件的布局偏移(如外边距或位置调整)、元素上方插入 DOM、图片或字体延迟加载,以及单页应用(SPA)的路由切换。
它支持哪些浏览器?它面向现代的、支持 ResizeObserver 的浏览器:Chrome 73+、Firefox 69+、Safari 13.1+ 和 Edge 79+,并且兼容移动端。
'contain' 和 'intersect' 模式有什么区别?'contain' 跟踪元素在其容器内的整体滚动进度,未设置元素时默认使用,适合进度条或视差效果。'intersect' 跟踪元素自身穿过视口时的可见性,设置了元素时默认使用,适合进入或离开动画以及懒加载。
它会取代原生 CSS 滚动驱动动画吗?不会,它是对原生方案的补充。ScrollMagic 提供跨浏览器支持、事件回调、进度值和状态管理,而原生滚动驱动 CSS API 目前在各平台上还没有全面覆盖这些能力。
我可以用插件扩展 ScrollMagic 吗?可以。它内置插件系统,可用于扩展实例的行为,完整的插件开发指南见 PLUGINS.md。
有没有我应该了解的旧版本?有。如果你需要旧版 API,可以使用仍在维护的 ScrollMagic 2,它位于 v2-stable 分支。
Creator
janpaepke
Size
6.5 MB
Stars
15K
Age
13y
Commits · 90 Days