ScrollMagic

repository·main·Indexed 12 days ago

https://github.com/janpaepke/scrollmagic

A lightweight, framework-agnostic library for managing scroll-driven interactions. Version 3.0.0-beta.5 wraps IntersectionObserver and ResizeObserver to provide precise scroll-position data and events like enter, leave, and progress. It supports both Contain and Intersect tracking modes and provides a plugin system for extending instance behavior.

Tokens
9.7K
Snippets
30
Records
47
Agent score
90%

What's inside ScrollMagic

  1. How ScrollMagic tracking works

    main

    ScrollMagic calculates progress (from 0 to 1) based on the relationship between two sets of bounds:

    1. Container bounds: A zone on the scroll container defined by containerStart and containerEnd.
    2. Element bounds: A zone on the tracked element defined by elementStart and elementEnd.

    Progress is tracked as the element bounds pass through the container bounds.

  2. Contain vs Intersect modes

    main

    ScrollMagic uses two primary mental models for tracking, determined by how the container bounds are positioned relative to the element:

    Contain

    Default when element is null. Container bounds match the viewport edges (containerStart and containerEnd are both at 'here' / 0%). Progress tracks while one fully contains the other (e.g., the element is fully inside the viewport, or the element fully covers the viewport). Typical uses: scroll progress bars, parallax, scroll-linked video.

    Intersect

    Default when element is set. Container bounds span the full viewport (containerStart and containerEnd are at 'opposite' edges / 100%). Progress tracks while the element intersects with the viewport (from the moment its leading edge enters until its trailing edge leaves). Typical uses: enter/leave animations, lazy loading, visibility tracking.

    Mapping to CSS Scroll-Driven Animations

    If you are familiar with native CSS view() timelines, here is the mapping:

    Native rangeScrollMagic equivalent
    coverintersect default — containerStart: 'opposite', containerEnd: 'opposite'
    containcontain default — containerStart: 0, containerEnd: 0
    entrycontainerStart: 'opposite', containerEnd: 0
    exitcontainerStart: 0, containerEnd: 'opposite'
  3. Understand the plugin hook sequence during destruction

    main

    The order in which hooks fire depends on how the instance is being stopped:

    • removePlugin(plugin): Fires onRemove only.
    • destroy() on an enabled instance:
      1. onDisable (tracking paused, observers disconnected)
      2. onDestroy (final cleanup)
    • destroy() on a disabled instance: Only onDestroy fires.

    Important: onRemove does not fire during destroy(). If your plugin requires the same cleanup logic for both removal and destruction, you should assign the same function to both hooks:

    const cleanup = function (this: ScrollMagic) {
    	/* ... */
    };
    const plugin: ScrollMagicPlugin = {
    	name: 'shared-cleanup',
    	onRemove: cleanup,
    	onDestroy: cleanup,
    };
  4. Use direction-agnostic utilities for plugins

    main

    When building plugins that need to support both vertical and horizontal scrolling, use the utilities provided in scrollmagic/util to handle direction-neutral properties and values.

    • agnosticProps(vertical): Returns a map of direction-neutral prop names (like start, end, size) to their CSS/DOM equivalents (top/left, bottom/right, height/width, etc.).
    • agnosticValues(vertical, obj): Extracts relevant values from an object (like a DOMRect) based on the scroll direction.

    Example usage:

    import { agnosticValues, agnosticProps } from 'scrollmagic/util';
    
    // For vertical scrolling:
    const values = agnosticValues(true, element.getBoundingClientRect());
    // returns { start: rect.top, end: rect.bottom, size: rect.height, ... }
  5. Quick Start with ScrollMagic

    main

    Import ScrollMagic and instantiate it with a target element. You can then chain .on() methods to listen for scroll events like enter, leave, and progress.

    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)}%`));
  6. Migrating from ScrollMagic v2 to v3

    main

    ScrollMagic v2 is in maintenance-only mode (final release 2.0.9). If you encounter issues or feature requests for v2, you are encouraged to migrate to v3. v3 is a native ES module with built-in TypeScript support and a different architectural approach.

    Key differences in v3 to consider during migration:

    • Architecture: Each new ScrollMagic({ element }) instance is self-contained; there is no global controller.
    • Pinning: v3 does not include a pinning system; use CSS position: sticky instead.
    • Animations: v3 is not an animation library; pair it with GSAP, Motion, or anime.js.
    • Scroll Containers: Use the scrollParent option to target window or any specific element.
    • Horizontal Scroll: Set vertical: false to enable horizontal scrolling.
    • Plugins: Use addPlugin() with onAdd, onRemove, and onModify hooks.
    • Positioning: Supports named shorthands like 'here' (0%), 'center' (50%), and 'opposite' (100%).
    • Events: Provides enter, leave, and progress events.
    • Lifecycle: Use enable() and disable() to pause/resume tracking without destroying the instance.
  7. When to call refresh()

    main

    While ScrollMagic automatically tracks size changes (via ResizeObserver) and scroll position changes, it cannot detect layout changes that affect an element's position without changing its size.

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

    • CSS changes: Modifying margin, padding, or position.
    • Class toggles: Adding/removing classes that affect layout.
    • DOM changes: Adding or removing siblings that shift the target element's position.
    • Resource loading: Images loading without explicit dimensions or fonts loading (which causes text reflow).
    • SPA Route changes: Content swaps that change the total scroll height.

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

    // 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());
  8. Create a ScrollMagic plugin

    main

    Plugins extend ScrollMagic instances with custom behavior such as class toggles, debug overlays, or animation bindings. A plugin is a plain object that must include a name and can optionally implement lifecycle hooks. Inside these hooks, this is bound to the ScrollMagic instance the plugin is attached to.

    To use a plugin, call sm.addPlugin(plugin) on your ScrollMagic instance.

    import ScrollMagic, { type ScrollMagicPlugin } from 'scrollmagic';
    
    const myPlugin: ScrollMagicPlugin = {
    	name: 'my-plugin',
    	onAdd() {
    		// `this` is the ScrollMagic instance
    		this.on('enter', () => {
    			/* ... */
    		});
    	},
    	onRemove() {
    		this.off('enter' /* ... */);
    	},
    };
    
    const sm = new ScrollMagic({ element: '#target' });
    sm.addPlugin(myPlugin);
    sm.removePlugin(myPlugin); // or let destroy() handle cleanup
  9. Provide a custom PixelConverter for dynamic offsets

    main

    For advanced offset calculations, you can provide a PixelConverter function instead of a number or string. A PixelConverter is a function that receives the current size of the element or container (in pixels) and returns a number representing the pixel offset to be used for position calculations.

    This is useful for offsets that need to scale dynamically based on the container's size.

    // A converter that always returns 10% of the container's size in pixels
    const myConverter: PixelConverter = (size: number) => size * 0.1;
    
    const options: Public = {
      containerStart: myConverter
    };
  10. Implement a ScrollMagic plugin

    main

    You can extend ScrollMagic functionality by creating a Plugin object. Plugins receive lifecycle callbacks that are automatically triggered by the instance. All callbacks are optional and the this context is the owning ScrollMagic instance.

    Available lifecycle hooks:

    • onAdd: Called when added via addPlugin.
    • onRemove: Called when removed via removePlugin.
    • onEnable: Called when enable() is called.
    • onDisable: Called when disable() is called.
    • onDestroy: Called when destroy() is called.
    • onModify: Called when options change via modify(). Receives only the changed options.
  11. Configure ScrollMagic options

    main

    All options are optional and can be passed to the constructor or updated via .modify().

    Inset values (used for elementStart, elementEnd, containerStart, containerEnd) work like CSS top/bottom. Positive values offset inward from the edge. Supported types:

    • Numbers: pixel values (e.g. 50)
    • Strings: percentage or pixel strings (e.g. '50%', '20px')
    • Named positions: 'here' (0%), 'center' (50%), 'opposite' (100%)
    • Functions: (size) => number for dynamic computation
    OptionTypeDefaultDescription
    elementElement | string | nullfirst child of containerThe tracked element (or CSS selector).
    elementStartnumber | string | function0Start inset on the element.
    elementEndnumber | string | function0End inset on the element.
    containerWindow | Element | string | nullwindowThe scroll container (or CSS selector).
    containerStartnumber | string | function | nullinferredStart inset on the scroll container.
    containerEndnumber | string | function | nullinferredEnd inset on the scroll container.
    verticalbooleantrueScroll axis (true = vertical, false = horizontal).