A simple and easy to use scroller
Source Code
Installation
Since our @manyone packages are private on npmjs you must authenticate to install it with npm login
.
If you’re not part of the Manyone organisation on npmjs.com, reach out to Caroline, Patrik or Lau.
pnpm add @manyone/animation
yarn add @manyone/animation
npm install @manyone/animation
Getting started
Import and add the Many scroller to your index.html in the bottom of the markup
< w-many-scroller ></ w-many-scroller >
import { WManyScroller } from ' @manyone/animation ' ;
customElements . define ( ' w-many-scroller ' , WManyScroller );
import { OptionRoot } from ' @manyone/animation ' ;
const fadeInObj : OptionRoot = {
const fadeIn = JSON . stringify (fadeInObj , null , 2 );
// Export your setups to make them accessible in components
export const useManyScrollerOptions = () => {
Use your setup in a component
import {useManyScrollerOptions} from " @/yourLocation/useManyScrollerOptions " ;
const { fadeIn } = useManyScrollerOptions ();
< div class = " comp-enter-fade " data-many-scroller = { fadeIn } >
< h1 > Fade in on enter </ h1 >
opacity : calc ( var ( --scroll-position-enter ) * 1 );
Go to examples
Animating in CSS
You are able to animate with CSS forwards animations and dynamic CSS variable values.
Forwards
Forwards animations are used as inview trigger animations with fixed duration.
CSS forwards animations are also great for hero intro animations, when the animation is done it’ll keep the styles from the last keyframe.
animate : example 1 s ease-in forwards ;
animate : example 1 s 1 s ease-out forwards ;
Progress variables
Progress variables is used to hook your animation into scroll positions
// Animates the component from opacity 0 to 1 based on the distance
opacity : calc ( 1 * var ( --scroll-position-enter ));
// Animates the component from 10rem down the page to 0rem based on the distance
transform : translateY ( calc ( 10 rem - ( var ( --scroll-position-full ) * 10 rem )))
// Animates the component from 10rem down the page to 0rem based on the distance when it enters
transform : translateY ( calc ( 10 rem - ( var ( --scroll-position-full ) * 10 rem )));
// Animates the component opacity to 0 when leaving the viewport
opacity : calc ( 1 - ( var ( --scroll-position-exit ) \ * 1 ));
Debug message
The debug message is created to easier navigate and iterate your values with designers while creating the page.
It’ll create a sticky debug element, with values used in your Many Scroller setup on the specific component.
Its a good idea to use the debug message for faster developing.
< w-many-scroller debug = " true " ></ w-many-scroller >
Option values
Option values are all the different types of values you are able to use while setting up your Many Scroller setup.
Types
inview
Everytime element is in view add .inview-(scrollerType) and remove the class when out of view.
inview-once
First time element is in view add .inview-(scrollerType).
progress
Everytime element is in view add .inview-(scrollerType) and remove the class when out of view.
Adds a dynamic scroll position variable to the element in percentage as a value from 0 to 1.
Numbers
distance and offsets
Numbers are a percentage of screen height.
[1] = 100% of screen height
[0.5] = 50% of screen height
[0] = 0% of screen height
breakpoints
Minimim window width in pixels
Event types
Progress
Fires events on scroll, while inview
Enter
Fires one event every time the element enters view
Enter-once
Fires one event first time the element enters view
Exit
Fires one event every time the element leaves view
Exit-once
Fires one event first time the element leaves view
There are 4 different scroll types: full, screen, enter and exit. Select the ones you feel suits your needs the best.
You are able to add multiple scroll types to one Many Scroller setup, if you want an enter animation and parallax etc.
Full
Full is using the entire screen height plus the height of the element. Its great for parallax, heros and sticky components.
Setup
const exampleObj : OptionRoot = {
type: // "inview" || "inview-once" || "progress"
const example = JSON . stringify (exampleObj , null , 2 );
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
Usage
opacity : calc ( 1 * var ( --scroll-position-full ));
// Can only be used if type is progress
document . addEventListener ( ' {eventID}-{eventType}-full ' , ( event ) => {
// Can only be used if event (Read about events)
Options
Properties Data type Values Default Value type String inview inview-once progress inview-once offsetStart Array of Numbers [0] offsetEnd Array of Numbers [0] event Array of Strings progress enter enter-once exit exit-once False
Screen
Screen is only related to screen height. Its great for animations needed to be done at an exact point on the users screen and triggering events; like highlighting a subnavigation when the content enters 25% of screen visibility.
Setup
const exampleObj : OptionRoot = {
type: // "inview" || "inview-once" || "progress"
const example = JSON . stringify (exampleObj , null , 2 );
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
Usage
opacity : calc ( 1 * var ( --scroll-position-screen ));
// Can only be used if type is progress
document . addEventListener ( ' {eventID}-{eventType}-screen ' , ( event ) => {
// Can only be used if event (Read about events)
Options
Properties Data type Values Default Value type String inview inview-once progress inview-once distance Array of Numbers [0] offsetStart Array of Numbers [0] event Array of Strings progress enter enter-once exit exit-once False
Enter
Enter is used when a object is entering the viewport. Its great for component enter animations like Fade In, Background color change etc.
Setup
const exampleObj : OptionRoot = {
type: // "inview" || "inview-once" || "progress"
const example = JSON . stringify (exampleObj , null , 2 );
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
Usage
opacity : calc ( 1 * var ( --scroll-position-enter ));
// Can only be used if type is progress
document . addEventListener ( ' {eventID}-{eventType}-enter ' , ( event ) => {
// Can only be used if event (Read about events)
Options
Properties Data type Values Default Value type String inview inview-once progress inview-once distance Array of Numbers [0] event Array of Strings progress enter enter-once exit exit-once False
Exit
Exit is used when a object is leaving the viewport. Its great for component exit animations like Fade Out.
Setup
const exampleObj : OptionRoot = {
type: // "inview" || "inview-once" || "progress"
const example = JSON . stringify (exampleObj , null , 2 );
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
Usage
opacity : calc ( 1 - ( 1 * var ( --scroll-position-exit )));
// Can only be used if type is progress
document . addEventListener ( ' {eventID}-{eventType}-exit ' , ( event ) => {
// Can only be used if event (Read about events)
Options
Properties Data type Values Default Value type String inview inview-once progress inview-once distance Array of Numbers [0] event Array of Strings progress enter enter-once exit exit-once False
Events
Events are used to trigger functions and acts like an observer. You can use it for highlighting subnavigations when enteringer or leaving content, trigger Spline animations etc.
The amount of event callbacks is decided by the type of event you choose, see the event options, on the scroller type you’ve selected.
Setup
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
const exampleObj : OptionRoot = {
event: [ ' progress ' , ' enter ' , ' enter-once ' , ' exit ' , ' exit-once ' ] ,
const example = JSON . stringify (exampleObj , null , 2 );
Create the event ID in your markup, its used for callbacks
Note: All events needs to be unique, so include an unique id like __iud from Storyblok.
data-many-scroller = " {example} "
data-many-scroller-event-id = " example-{__uid} "
Usage
// scrollerType = "full", in this example
document . addEventListener ( ' example-{__uid}-{scrollerType}-{eventType} ' , ( event ) => {
// In this example the event.detail will follow your CSS variable and return a number between 0-1.
myProgressFunction (event . detail );
// If you dont want to fire the event all the time use one of the other options and just trigger the function
Remember to destroy your eventListener when switching page
Breakpoints
You are able to use breakpoints in pixels based on screen width. This allows you the change distances and/or offsets for mobile/tablet/desktop.
If you have more breakpoints than distance/offsets values, it’ll use the last availble value.
NOTE: Don’t add breakpoints if you are not changing your distance/offset values.
Setup
const exampleObj : OptionRoot = {
breakpoints: [ 320 , 1024 ] ,
offsetStart: [ 0.5 , 0.25 ] ,
const example = JSON . stringify (exampleObj , null , 2 );
Scroll direction is default in Many Scroller. It can be used to show/hide navigation while scrolling up or down.
Usage
The scroll direction is set on the html-tag as a data attribute
[ data-scrolldir = ' down ' ] {
Examples
Here is some inspiration
[CSS-Forwards] Inview cards
See the example here .
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " horisontal-scroll relative " data-many-scroller = {example} >
< div class = " transform-wrapper w-full gap-5 top-0 left-0 flex items-center p-10 " >
< div class = " image-wrapper flex items-center " >
< img src = " /manyScrollerAssets/energy.jpeg " />
< div class = " image-wrapper flex items-center " >
< img src = " /manyScrollerAssets/energy.jpeg " />
< div class = " image-wrapper flex items-center " >
< img src = " /manyScrollerAssets/energy.jpeg " />
transform : translateY ( 1 rem );
@for $i from 1 through 3 {
& .image-wrapper:nth-child ( #{ $i } ) {
transform : translateY ( 0 rem );
transition : opacity 0.5 s calc ( 0.15 s * #{ $i } ) ease-in-out , transform 0.5 s calc ( 0.15 s * #{ $i } ) ease-in-out ;
[CSS-Progress] Enter and exit fade
See the example here .
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " enter-exit-fade " data-many-scroller = {example} >
< div class = " clamp-p-20-40 flex gap-10 " >
< img src = " /manyScrollerAssets/energy.jpeg " class = " w-1/2 " />
< p class = " w-1/2 clamp-text-10-20 " > Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book </ p >
opacity : calc (( var ( --scroll-position-enter ) * 1 ));
opacity : calc ( 1 - ( var ( --scroll-position-exit ) * 1 ));
[CSS-Progress] Parallax (Example: 1)
See the example here .
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " parallax-1 " data-many-scroller = {example} >
< div class = " image-wrapper clamp-m-20-40 flex gap-10 " >
< img src = " /manyScrollerAssets/energy.jpeg " />
transform : translateY ( calc ( -15 vh + ( var ( --scroll-position-full ) * 30 vh )));
[CSS-Progress] Parallax (Example: 2)
See the example here .
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " parallax-2 relative " data-many-scroller = {example} >
< div class = " clamp-p-20-40 flex " >
< div class = " image-wrapper w-50% " >
< img src = " /manyScrollerAssets/energy.jpeg " class = " img-left " />
< div class = " image-wrapper w-50% " >
< img src = " /manyScrollerAssets/energy.jpeg " class = " img-right " />
margin : 2.5 rem calc ( 2.5 rem - ( var ( --scroll-position-screen ) * 2.5 rem ));
transform : translateY ( calc ( -15 vh + ( var ( --scroll-position-enter ) * 15 vh )));
object-position : left bottom ;
object-position : right bottom ;
See the example here .
const exampleObj : OptionRoot = {
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " horisontal-scroll relative h-400vh " data-many-scroller = {example} >
< div class = " transform-wrapper w-full h-100vh sticky top-0 left-0 inline-flex items-center " >
< div class = " image-wrapper w-50vw min-w-50vw flex items-center p-x-5 p-l-10 " >
< img src = " /manyScrollerAssets/energy.jpeg " />
< div class = " image-wrapper w-50vw min-w-50vw flex items-center p-x-5 " >
< img src = " /manyScrollerAssets/energy.jpeg " />
< div class = " image-wrapper w-50vw min-w-50vw flex items-center p-x-5 " >
< img src = " /manyScrollerAssets/energy.jpeg " />
< div class = " image-wrapper w-50vw min-w-50vw flex items-center p-x-5 p-r-10 " >
< img src = " /manyScrollerAssets/energy.jpeg " />
transform : translateX ( calc (( -100 % ) * var ( --scroll-position-full )));
[Event] Enter and exit canvas event
See the example here .
const exampleObj : OptionRoot = {
event: [ ' enter ' , ' exit ' ] ,
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " h-100vh " data-many-scroller = {example} data-many-scroller-event-id = " event " >
< canvas id = " canvas " class = " w-full h-100vh sticky top-0 left-0 " ></ canvas >
const canvas = document . getElementById ( " canvas " ) as HTMLCanvasElement ;
const ctx = canvas . getContext ( " 2d " );
const boxWidth = window . innerWidth / 8 ;
const boxHeight = window . innerWidth / 8 ;
let x = (canvas . width - boxWidth) / 2 ;
let y = (canvas . height - boxHeight) / 2 ;
function resizeCanvas () {
canvas . width = window . innerWidth ;
canvas . height = window . innerHeight ;
x = (canvas . width - boxWidth) / 2 ;
y = (canvas . height - boxHeight) / 2 ;
window . addEventListener ( " resize " , resizeCanvas);
let requestAnim : number | undefined ;
console . log ( ' Animate running ' )
ctx . clearRect ( 0 , 0 , canvas . width , canvas . height );
img . src = " /manyScrollerAssets/manyone_ticker.webp " ;
ctx . fillStyle = " transparent " ;
ctx . fillRect (x , y , boxWidth , boxHeight);
ctx . drawImage (img , x , y , boxWidth , boxHeight);
if (x <= 0 || x + boxWidth >= canvas . width ) {
if (y <= 0 || y + boxHeight >= canvas . height ) {
requestAnim = requestAnimationFrame (animate);
document . addEventListener ( ' event-full-enter ' , () => {
document . addEventListener ( ' event-full-exit ' , () => {
if (requestAnim !== undefined ) {
cancelAnimationFrame (requestAnim);
[Event] Progress Canvas
See the example here .
const exampleObj : OptionRoot = {
event: [ ' progress ' , ' enter ' , ' exit ' ] ,
const example = JSON . stringify (exampleObj , null , 2 );
< div class = " h-200vh " data-many-scroller = {example} data-many-scroller-event-id = " event " >
< canvas id = " canvas " class = " w-full h-100vh sticky top-0 left-0 " ></ canvas >
const canvas = document . getElementById ( " canvas " ) as HTMLCanvasElement ;
const ctx = canvas . getContext ( " 2d " );
let boxWidth = (window . innerWidth / 8 );
let boxHeight = ( window . innerWidth / 8 ) ;
let x = (canvas . width - boxWidth) / 2 ;
let y = (canvas . height - boxHeight) / 2 ;
function resizeCanvas () {
canvas . width = window . innerWidth ;
canvas . height = window . innerHeight ;
x = (canvas . width - boxWidth) / 2 ;
y = (canvas . height - boxHeight) / 2 ;
window . addEventListener ( " resize " , resizeCanvas);
let requestAnim : number | undefined ;
console . log ( ' Animate running ' )
ctx . clearRect ( 0 , 0 , canvas . width , canvas . height );
img . src = " /manyScrollerAssets/manyone_ticker.webp " ;
ctx . fillStyle = " transparent " ;
ctx . fillRect (x , y , boxWidth , boxHeight);
ctx . drawImage (img , x , y , boxWidth , boxHeight);
boxWidth = (window . innerWidth / 8 ) * scrollScale;
boxHeight = (window . innerWidth / 8 ) * scrollScale;
if (x <= 0 || x + boxWidth >= canvas . width ) {
if ( x + boxWidth >= canvas . height ) {
x = x - (x + boxWidth - canvas . width );
if (y <= 0 || y + boxHeight >= canvas . height ) {
if ( y + boxHeight >= canvas . height ) {
y = y - (y + boxHeight - canvas . height );
requestAnim = requestAnimationFrame (animate);
document . addEventListener ( ' event-full-progress ' , ( event : Event ) => {
scrollScale = (event as CustomEvent < number >) . detail * 2 + 1 ;
document . addEventListener ( ' event-full-enter ' , () => {
document . addEventListener ( ' event-full-exit ' , () => {
if (requestAnim !== undefined ) {
cancelAnimationFrame (requestAnim);