scroll

scroll is a 2.5kb function for creating scroll-linked animations.

It works with both animate and timeline:

scroll(animate("section", { opacity: [0, 1] }))

scroll(timeline(sequence))

It also accepts normal functions so you can animate anything, like video progress, canvas and Three.js:

scroll(({ y }) => {
  video.currentTime = video.duration * y.progress
})

By default, it updates on window scroll. It's also possible to animate based on the scroll of another element:

scroll(animate(element, { opacity: [0, 1] }), {
  container: document.querySelector("#carousel"),
})

And on the position of an element within the container:

scroll(animate(element, { opacity: [0, 1] }), {
  target: element,
  offset: ScrollOffset.Enter,
})

Note: Scroll triggered animations are performed with the inView function.

Usage

Import scroll from Motion One:

import { scroll } from "motion"

Window scroll

By default, animations and functions passed to scroll will be scrubbed based on the scroll progress of the window.

import { scroll, animate } from "motion"

// Pass an animation or timeline to automatically scrub
scroll(
  animate(".progress-bar", { scaleX: [0, 1] })
);

// Or pass a function to directly use the scroll info
const progress = document.querySelector(".progress")
scroll(({ y }) => {
  progress.innerHTML = y.progress.toFixed(2)
});

Element scroll

By setting container to a scrollable element, they'll be scrubbed based on the scroll progress of that element.

import { scroll, animate } from "motion"

const carousel = document.querySelector("ul")

// Pass an animation or timeline to automatically scrub
scroll(
  animate(".progress-bar", { scaleX: [0, 1] }),
  { container: carousel, axis: "x" }
);

// Or pass a function to directly use the scroll info
const progress = document.querySelector(".progress")
scroll(
  ({ x }) => progress.innerHTML = x.progress.toFixed(2),
  { container: carousel, axis: "x" }
);

Element position

By default, scroll progress is determined by tracking the position of the scrollable area within the viewport of the container/window.

It's also possible to track another element as it moves within that scroll container by setting it to target.

In this next example we've set the target to be the second item in the list (the yellow box).

import { scroll, animate } from "motion"

const secondItem = document.querySelectorAll("section div")[1]

const scrollOptions = {
  target: secondItem,
  offset: ["start end", "end end"]
}

// Pass an animation or timeline to automatically scrub
scroll(
  animate(".progress-bar", { scaleX: [0, 1] }),
  scrollOptions
);

// Or pass a function to directly use the scroll info
const progress = document.querySelector(".progress")
scroll(
  ({ y }) => progress.innerHTML = y.progress.toFixed(2),
  scrollOptions
);

Scroll offsets

Notice also how in the previous example we've set the offset option.

Just as offset is used in animate to map keyframes to time, scroll offsets are used to map keyframes to scroll position.

Specifically, scroll position in terms of the intersection between the target and container.

A each offset defines where the target will meet the container. So for example, "start end" means when the start of the target meets the end of the container.

We can define multiple offsets to map a range of scroll positions to more than just the start and end of an animation.

Accepted values

Both target and container points can be defined as:

  • Number: A value where 0 represents the start of the axis and 1 represents the end. So to define the top of the target with the middle of the container you could define "0 0.5". Values outside this range are permitted.
  • Pixels: Pixel values like "100px", "-50px" will be defined as that number of pixels from the start of the target/container.
  • Viewport: "vh" and "vw" values are accepted.
  • Names: "start", "center" and "end" can be used as clear shortcuts for 0, 0.5 and 1 respectively.
  • Percent: Same as raw numbers but expressed as "0%" to "100%".

Shorthand

It's also possible to define just one value for both target/container.

Named values are repeated, so "center" is equivalent to "center center" and "end" is equivalent to "end end".

Numerical values like "100px" are applied to the target only, so "100px" would define when 100 pixels from the top of the target meets the start of the container.

Resolved offsets

All offsets get resolved into pixel values, and these pixel values are passed to the scroll callback (if one is provided) as the offset value on info.x or info.y.

scroll(
  (info) => {
    console.log(info.y.offset) // e.g. [100, 900]
  },
  {
    target: document.querySelector("article"),
    offset: ["start end", "end end"],
  }
)

Predefined offsets

Motion One includes some pre-defined scroll offsets.

import { scroll, ScrollOffset } from "motion"
scroll(animation, { target: element, offset: ScrollOffset.Enter })

It includes four offsets:

  • Enter: ["start end", "end end"]
  • Exit: ["start start", "end start"]
  • Any: ["end start", "start end"]
  • All: ["start start", "end end"]

For instance, here's an animation that will fade an element out as it leaves the start/end of the viewport:

scroll(animate(element, { opacity: [0, 1, 1, 0] }), {
  target: element,
  offset: [...ScrollOffset.Enter, ...ScrollOffset.Exit],
})

Function callback

scroll can be passed a function that will run whenever the scroll position changes:

scroll(
  (info) => {},
  { target: element, offset: ScrollOffset.Enter }
)

This way you can animate anything via scroll position, like video playback or Three.js.

This callback is provided an info object which contains:

  • time: The time the scroll position was recorded.
  • x: Info on the x scroll axis.
  • y: Info on the y scroll axis.

Each individual axis is an object containing the following data:

  • current: The current scroll position.
  • offset: The scroll offsets resolved as pixels.
  • progress: A progress value, between 0-1 of the scroll within the resolved offsets.
  • scrollLength: The total scrollable length on this axis. If target is the default scrollable area, this is the container scroll length minus the container length.
  • velocity: The velocity of the scroll on this axis.

Options

container

Default: Viewport

The scrollable container to track the scroll position of. By default, this is the window viewport. But it can be any scrollable element.

scroll(animation, { container: document.getElementById("carousel") })

target

Default: Scrollable area of container

By default, this is the scrollable area of the container. It can additionally be set as another element so we can track its progress within the viewport.

scroll(animate(element, { opacity: [0, 1] }), {
  target: element,
  offset: ["start end", "end end"], // When target enters container
})

axis

Default: "y"

The axis of scroll to track to apply offsets to and use as animation progress.

offset

Default: ["start start", "end end"]

A list of scroll offsets to use for resolving scroll progress.

Examples

Page progress bar

Element-based progress bar

Parallax and scroll-snapping

Fade on enter/exit

Pinning

Video scrubbing

3D camera