Skip to main content

Migration Guide: v4 to v5

This guide will help you migrate from Locomotive Scroll v4 to v5. Version 5 is a complete rewrite built on Lenis, offering better performance, smaller bundle size, and improved TypeScript support.

tip

Locomotive Scroll v5 is simpler and more performant than v4, but it requires some changes to your code. Take your time to understand the new patterns.

Overview of Changes

What's New

  • Built on Lenis — Modern smooth scroll engine instead of Virtual Scroll
  • Smaller Bundle — 9.4kB (v5) vs ~12.1kB (v4) gzipped
  • TypeScript First — Fully typed with better autocomplete
  • Simplified API — Fewer options, clearer patterns
  • Better Performance — Dual Intersection Observer strategy
  • Custom Scroll Containers — Support for contained scrolling
  • Native Events — CustomEvents instead of ModularJS

What's Removed

  • ModularJS support — Use native CustomEvents instead
  • data-scroll-container — No longer required
  • data-scroll-section — No longer needed for performance
  • Custom scrollbar — Use Lenis scrollbar or native scrollbar
  • data-scroll-sticky — Use CSS position: sticky instead
  • data-scroll-delay — No longer available
  • data-scroll-direction — Parallax follows scroll orientation
  • data-scroll-target — No longer available

Installation

v4

npm install locomotive-scroll@4.1.4

v5

npm install locomotive-scroll

Basic Setup

v4: Required Container

<div data-scroll-container>
<div data-scroll-section>
<h1 data-scroll>Hey</h1>
</div>
</div>
import LocomotiveScroll from 'locomotive-scroll';

const scroll = new LocomotiveScroll({
el: document.querySelector('[data-scroll-container]'),
smooth: true,
});

v5: Simpler Setup

<main>
<h1 data-scroll>Hey</h1>
</main>
import LocomotiveScroll from 'locomotive-scroll';

const scroll = new LocomotiveScroll();
info

In v5, smooth scrolling is enabled by default through Lenis. No need for data-scroll-container or data-scroll-section.


Instance Options

v4 → v5 Mapping

v4 Optionv5 EquivalentNotes
ellenisOptions.wrapperNow inside lenisOptions
smooth✅ Always enabledSmooth scrolling is default behavior
directionlenisOptions.orientationValues: 'vertical' or 'horizontal'
lerplenisOptions.lerpSame concept, inside lenisOptions
multiplierlenisOptions.wheelMultiplierRenamed for clarity
touchMultiplierlenisOptions.touchMultiplierSame, inside lenisOptions
class❌ RemovedUse data-scroll-class attribute instead
offsettriggerRootMarginNow uses IntersectionObserver rootMargin format
repeatdata-scroll-repeatNow per-element attribute
initClass❌ RemovedNo longer needed
scrollingClass❌ RemovedUse scroll event to detect scrolling
getDirection✅ AvailableAccess via lenisInstance.direction
getSpeed✅ AvailableAccess via lenisInstance.velocity
scrollbarContainer❌ RemovedUse Lenis scrollbar or native
scrollbarClass❌ RemovedUse Lenis scrollbar or native

v4 Example

const scroll = new LocomotiveScroll({
el: document.querySelector('[data-scroll-container]'),
smooth: true,
direction: 'vertical',
lerp: 0.1,
multiplier: 1,
class: 'is-inview',
scrollbarClass: 'c-scrollbar',
});

v5 Equivalent

const scroll = new LocomotiveScroll({
lenisOptions: {
orientation: 'vertical',
lerp: 0.1,
wheelMultiplier: 1,
},
});

// Access Lenis directly for more control
console.log(scroll.lenisInstance.direction); // Get direction
console.log(scroll.lenisInstance.velocity); // Get speed

Data Attributes

Removed Attributes

v4 Attributev5 Alternative
data-scroll-container❌ Remove it (not needed)
data-scroll-section❌ Remove it (not needed)
data-scroll-stickyUse CSS position: sticky
data-scroll-delay❌ Not available
data-scroll-direction❌ Follows scroll orientation
data-scroll-target❌ Not available

Modified Attributes

Attributev4 Behaviorv5 Behavior
data-scroll-speedParallax effect with arbitrary valuesRecalculated based on scroll container size (see details below)
data-scroll-callTriggers ModularJS methodNow triggers native CustomEvent
data-scroll-offset"bottom,top" format✅ Same format
data-scroll-positionBased on window positionNow based on Lenis scroll container position

New Attributes

AttributeDescription
data-scroll-css-progressAdds --progress CSS variable (0-1)
data-scroll-event-progressEmits progress via CustomEvent
data-scroll-enable-touch-speedEnable parallax on touch devices

Parallax

v4

<div data-scroll data-scroll-speed="2">Fast parallax</div>

v5

<!-- Parallax disabled on touch by default -->
<div data-scroll data-scroll-speed=".5">Fast parallax (desktop only)</div>

<!-- Enable on touch devices -->
<div data-scroll data-scroll-speed=".5" data-scroll-enable-touch-speed>
Fast parallax (all devices)
</div>
Key Changes
  1. Parallax is automatically disabled on touch devices in v5 to use native scrolling. Use data-scroll-enable-touch-speed to override this behavior.
  2. Speed calculation completely redesigned - Now based on scroll container size, not arbitrary values.

How data-scroll-speed Works in v5

The parallax calculation has been completely redesigned in v5 to be more predictable and based on the scroll container size.

Formula:

translateValue = progress × containerSize × speed × -1

Where:

  • containerSize = Height (vertical) or width (horizontal) of Lenis scroll container
  • progress ranges from -1 to 1 for normal elements
  • progress ranges from 0 to 1 for elements visible on page load (in fold)

Examples:

With data-scroll-speed="1" and vertical scrolling:

  • Normal element: Moves from +containerHeight to -containerHeight (total displacement = 2× container height)
  • In-fold element: Moves from 0 to -containerHeight (total displacement = 1× container height)

With data-scroll-speed="0.5":

  • Normal element: Moves from +0.5×containerHeight to -0.5×containerHeight (total = 1× container height)
  • In-fold element: Moves from 0 to -0.5×containerHeight (total = 0.5× container height)

With data-scroll-speed="-1" (reversed):

  • Normal element: Moves from -containerHeight to +containerHeight (opposite direction)
Migration Tip

v4 to v5 speed conversion:

  • v4 speeds were arbitrary values
  • v5 speeds are relative to container size
  • Start with smaller values (0.1 to 0.5) and adjust based on container size
  • Test with different viewport sizes since it's now proportional

Events & Callbacks

v4: ModularJS Integration

<div data-scroll data-scroll-call="myMethod, MyModule">Trigger</div>
scroll.on('call', (func) => {
this.call(...func); // ModularJS
});

v5: Native CustomEvents

<div data-scroll data-scroll-call="videoTrigger">Trigger</div>
window.addEventListener('videoTrigger', (e) => {
const { target, way, from } = e.detail;
// way: 'enter' | 'leave'
// from: 'start' | 'end'

if (way === 'enter') {
target.querySelector('video').play();
}
});
Migration Tip

Replace all ModularJS data-scroll-call patterns with native CustomEvent listeners. It's simpler and has no dependencies!


Scroll Events

v4

scroll.on('scroll', (args) => {
console.log(args.scroll.y);
console.log(args.direction);
console.log(args.speed);
});

v5

const scroll = new LocomotiveScroll({
scrollCallback: ({ scroll, velocity, direction, progress }) => {
console.log(scroll); // Current scroll position
console.log(velocity); // Scroll speed
console.log(direction); // 1 (down/right), -1 (up/left), 0 (stopped)
console.log(progress); // 0 to 1
},
});

Progress Tracking

v4: Using data-scroll-id

<h1 data-scroll data-scroll-id="hero">Hero</h1>
scroll.on('scroll', (args) => {
if (args.currentElements['hero']) {
let progress = args.currentElements['hero'].progress;
console.log(progress); // 0 to 1
}
});

v5: Using Progress Events

<h1 data-scroll data-scroll-event-progress="heroProgress">Hero</h1>
window.addEventListener('heroProgress', (e) => {
console.log(e.detail.progress); // 0 to 1
});

v5: Using CSS Variables

<h1 data-scroll data-scroll-css-progress>Hero</h1>
[data-scroll-css-progress] {
opacity: calc(var(--progress) * 1);
transform: translateY(calc((1 - var(--progress)) * 100px));
}

Methods

Updated Methods

Methodv4v5Notes
destroy()Same API
start()Same API
stop()Same API
scrollTo(target)Delegated to Lenis
update()❌ Renamed to resize()Manual resize trigger
init()❌ RemovedNo longer needed

v4

scroll.update();
scroll.destroy();
scroll.init(); // Reinitialize

v5

scroll.resize(); // Rarely needed (auto-synced with Lenis)
scroll.destroy();
// No init() - create new instance instead

Sticky Elements

v4: Using data-scroll-sticky

<div data-scroll data-scroll-sticky data-scroll-target="#target">
Sticky element
</div>

<div id="target">Target</div>

v5: Use CSS position: sticky

<div class="sticky-element">Sticky element</div>
.sticky-element {
position: sticky;
top: 0;
}
Why?

CSS position: sticky is performant, native, and doesn't require JavaScript. v5 removed custom sticky implementation in favor of the native solution.


Custom Scroll Containers

v4: Not Supported

In v4, you could only do full-page scrolling.

v5: Fully Supported

const scroll = new LocomotiveScroll({
lenisOptions: {
wrapper: document.querySelector('.scroll-container'),
content: document.querySelector('.scroll-content'),
},
});
<div class="scroll-container" style="height: 100vh; overflow: hidden;">
<div class="scroll-content">
<div data-scroll data-scroll-speed="0.5">Parallax works here too!</div>
</div>
</div>

Performance Improvements

v4 Performance Patterns

<!-- Recommended: Split into sections for better performance -->
<div data-scroll-container>
<div data-scroll-section>
<!-- Content here -->
</div>
<div data-scroll-section>
<!-- More content -->
</div>
</div>

v5 Performance (Automatic)

v5 uses a dual Intersection Observer strategy that automatically optimizes performance:

  • Trigger IO — For simple in-view detection (classes, callbacks)
  • RAF IO — For continuous animations (parallax, progress)

Elements only subscribe to RAF when:

  1. They're visible (detected by IO)
  2. They need continuous updates (parallax, progress tracking)

No manual optimization needed!


Complete Migration Checklist

HTML Changes

  • Remove all data-scroll-container attributes
  • Remove all data-scroll-section attributes
  • Remove data-scroll-sticky → use CSS position: sticky
  • Remove data-scroll-delay → no replacement
  • Remove data-scroll-direction → follows scroll orientation
  • Remove data-scroll-target → no replacement
  • Update ModularJS data-scroll-call → use simple event names
  • Consider adding data-scroll-enable-touch-speed for mobile parallax

JavaScript Changes

  • Remove el option → use lenisOptions.wrapper if needed
  • Remove smooth option → always enabled
  • Rename directionlenisOptions.orientation
  • Move scroll options into lenisOptions object
  • Replace scroll.on('call', ...)window.addEventListener(eventName, ...)
  • Replace scroll.on('scroll', ...)scrollCallback option
  • Replace scroll.update()scroll.resize() (rarely needed)
  • Remove scroll.init() → recreate instance instead
  • Remove custom scrollbar code → use native or Lenis scrollbar

CSS Changes

  • Remove locomotive-scroll v4 CSS
  • Add locomotive-scroll v5 CSS (minimal)
  • Replace sticky elements → use position: sticky
  • Update any custom scrollbar styles → use Lenis scrollbar

Example: Complete v4 to v5 Migration

Before (v4)

<div data-scroll-container>
<div data-scroll-section>
<h1 data-scroll data-scroll-id="hero">Hero</h1>
<div data-scroll data-scroll-speed="2">Parallax</div>
<div data-scroll data-scroll-call="videoPlay, VideoModule">Video</div>
<div data-scroll-sticky data-scroll-target="#end">Sticky</div>
</div>
<div id="end" data-scroll-section>End</div>
</div>
import LocomotiveScroll from 'locomotive-scroll';

const scroll = new LocomotiveScroll({
el: document.querySelector('[data-scroll-container]'),
smooth: true,
direction: 'vertical',
lerp: 0.1,
class: 'is-inview',
});

scroll.on('scroll', (args) => {
if (args.currentElements['hero']) {
console.log(args.currentElements['hero'].progress);
}
});

scroll.on('call', (func) => {
this.call(...func); // ModularJS
});

After (v5)

<main>
<h1 data-scroll data-scroll-event-progress="heroProgress">Hero</h1>
<div data-scroll data-scroll-speed="2">Parallax</div>
<div data-scroll data-scroll-call="videoPlay">Video</div>
<div class="sticky">Sticky</div>
<div>End</div>
</main>
import LocomotiveScroll from 'locomotive-scroll';

const scroll = new LocomotiveScroll({
lenisOptions: {
orientation: 'vertical',
lerp: 0.1,
},
scrollCallback: ({ scroll, velocity, direction, progress }) => {
// Global scroll callback if needed
},
});

// Progress tracking
window.addEventListener('heroProgress', (e) => {
console.log(e.detail.progress);
});

// Video callback
window.addEventListener('videoPlay', (e) => {
const { target, way } = e.detail;
if (way === 'enter') {
target.querySelector('video').play();
}
});
.sticky {
position: sticky;
top: 0;
}

Need Help?

Happy scrolling with v5! 🚀