npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@kaliber/scroll-progression

v1.1.0

Published

Track the scroll progression between two points within a scroll parent as a normalized number between 0 and 1. Then use this value as input for animations!

Downloads

24

Readme

@kaliber/scroll-progression

Track the scroll progression between two points within an scroll parent as a normalized number between 0 and 1.

Motivation

Animating things in reaction to scroll should be easy, also if you don't want to use GSAP's ScrollTrigger library. This library tracks you scroll progression and calls you back with a value between 0 and 1. This is perfect for animation, since you can apply easing functions to it and use linear interpolation (lerp) to map it to any output domain that you like. Total freedom!

Contents

Installation

yarn add @kaliber/scroll-progression @kaliber/math

Transpilation

When working with @kaliber/build, add /@kaliber\/scroll-progression/ to your compileWithBabel array.

Polyfills

  • ResizeObserver

Usage

This library tracks to progression of a given element between two points within its scroll parent. These two points are called scroll triggers.

The scroll parent

The scroll parent of an element is found by finding the closest parent that has an overflow or overflow-y value of auto or scroll.

Scroll triggers

All the functions defined in @kaliber/scroll-progression/triggers return an object consisting of a fraction and (optionally) an offset. The fraction is a relative value based on the scroll parent's height. 0 means the top of the scroll parent, 1 means the bottom. If you need to increase or decrease with an amount of pixels, you can use the offset property for this.

For instance: a scroll trigger describing the point 100 pixels below the top of an element is described as follows:

{ fraction: 0, offset: 100 }

To improve readability, a set of triggers is exported which describe the most common scroll triggers:

| Function | | |---|---| | top | (offset = 0) => ({ fraction: 0, offset }) | | center | (offset = 0) => ({ fraction: 0.5, offset: 0 }) | | bottom | (offset = 0) => ({ fraction: 1, offset: 0 }) | | custom | (fraction, offset = 0) => ({ fraction, offset }) |

Examples

onScrollProgression({
   // The element we are determining the scroll progression for
  element,

  // The starting position (everything before this point results in 0)
  start: {
    // The start location relative to the element (in this case the top of the element)
    element: { fraction: 0, offset: 0 }, 

    // The start location relative to the scroll parent (in this case the bottom of the scroll parent)
    scrollParent: { fraction: 1, offset: 0 },
  },

  // The end position (everything after this point results in 1)
  end: {
    // The end location relative to the element (in this case the bottom of the element)
    element: { fraction: 1, offset: 0 },

    // The end location relative to the element (in this case the top of the scroll parent)
    scrollParent: { fraction: 0, offset: 0 },
  },

  // Callback, called when the progression (a value between 0 and 1) changes
  onChange(progression) {
    ...
  }
})

In the above example we are stating that we want to know the progression of the element from when starts to become visible at the bottom of the scroll parent (a progression near 0) until it is invisible at top of the scroll parent (a progression near 1).

The progression in this example is used to track the position in the scroll parent from the moment it reaches the bottom all the way until it reaches the top. So as the element visually moves up (scrolling down) the progression will go from 0 to 1.

In other words: we start when the top of the element touches the bottom of the scrollParent and we end when the bottom of the element touches the top of the scrollParent.

Below are some more examples to help you get a feeling for this. To view some examples in a practical setting, check the /example folder!

Parallax scrolling

When parallax scrolling you want to animate whenever the element is visible, also when only just a fraction has entered/left the scroll parent. Specifically:

  • Start when the top of the element reaches the bottom of the scroll parent
  • End when the bottom of the element reaches the top of the scroll parent
import { onScrollProgression, triggers } from '@kaliber/scroll-progression'

const cleanup = onScrollProgression({
  element: component,
  start: { element: triggers.top(), scrollParent: triggers.bottom() },
  end: { element: triggers.bottom(), scrollParent: triggers.top() },
  onChange(progression) { 
    /* setTranslateY(lerp({ start: -10%, end: 10%, input: progression })) */ 
  }
})

Playing video/animation on scroll

When controlling a video or animation, you want the user to be able to view the whole video/animation. Therefore you start tracking when the element is scrolled fully into view. You finishing tracking when the element reaches the top of the screen, but is still fully visible.

  • Start when the bottom of the element reaches the bottom of the scroll parent
  • End when the top of the element reaches the top of the scroll parent
import { onScrollProgression, triggers } from '@kaliber/scroll-progression'

const { ref } = onScrollProgression({
  element: component,
  start: { element: triggers.bottom(), scrollParent: triggers.bottom() },
  end: { element: triggers.top(), scrollParent: triggers.top() },
  onChange(progression) {
    /* updateVideoProgress(progression) */ 
  }
})

Scroll reveals

Animations start when the elements become visible, scrolling them into view. They are finished when they are still visible within the scroll parent. Specifically:

  • Start when the top of the element reaches the bottom of the scroll parent
  • End when the top of the element reaches 200 pixels above the bottom of the scroll parent
import { onScrollProgression, triggers } from '@kaliber/scroll-progression'

onScrollProgression({
  element: component,
  start: { element: triggers.bottom(), scrollParent: triggers.bottom() },
  end: { element: triggers.top(), scrollParent: triggers.bottom(-200) },
  onChange(progression) { 
    /* setOpacity(progression) */ 
  }
})

Custom triggers

If the predefined triggers aren't exactly what you need, you can define your own. Consider the following case:

  • Start when the top of the element reaches the bottom of the scroll parent
  • End when the top of the element reaches the point 200 pixels above 75% of the scroll parent, measured from the top of the scroll parent
import { onScrollProgression, triggers } from '@kaliber/scroll-progression'

const { ref } = onScrollProgression({
  element: component,
  start: { element: triggers.bottom(), scrollParent: triggers.bottom() },
  end: { element: triggers.top(), scrollParent: triggers.custom(0.75, -200) },
  onChange(progression) { 
    /* setOpacity(progression) */ 
  }
})

Usage with React

import { useScrollProgression, triggers } from '@kaliber/scroll-progression'

const trackedElementRef = useScrollProgression({
  start: { element: triggers.top(), scrollParent: triggers.bottom() },
  end: { element: triggers.bottom(), scrollParent: triggers.top() },
  onChange(progression) { /* Do something */ }
})

Usage with react-spring

If you always use useScrollProgression together with react-spring it might reduce noise if you implement the following custom hook:

function useAnimatedScrollProgression({ start, end, getSpringProps }) {
  const [spring, springApi] = useSpring(() => ({ 
    ...getSpringProps(0), 
    config: { tension: 500, friction: 35 } 
  }))

  const { ref } = useAnimatedScrollProgression({
    start,
    end,
    onChange(input) {
      spring.start(getSpringProps(input)) })
    }
  })

  return { ref, spring }
}

Usage:

const { ref, spring } = useAnimatedScrollProgression({
  start: { element: triggers.top(), scrollParent: triggers.bottom() },
  end: { element: triggers.top(), scrollParent: triggers.center() },
  getSpringProps: x => ({
    opacity: x,
    scale: lerp({ start: 0.5, end: 1, input: easeOut(x) })
  })
})

Tips & Gotcha's

Do NOT use vh units in your page

This might be a hard one, but when using scroll-based animations, you should really avoid using vh units. These re-layout your whole page when resizing leading to noticable jag on mobile. On mobile this resizing is triggered often (when your navigation bar(s) show or hide) which is why this is an issue.

The introduction of large and small viewport units will resolve this issue.

Optimize performance with css contain

If you have a large page with animated components, you might start to notice some performance issues. If this is the case, you can mark you animated components with contain: layout paint style;. This allows the browser to make certain optimizations during recalculation. Leave out properties which cause issues, for instance paint if you component overflows it's bounding box.

🚨 Gotcha: If you put contain: layout; on an element, don't but the ref returned by useScrollProgression on the same element.

Transforms

This library uses getBoundingClientRect() to determine to position of objects on the screen. If you translate objects (or parents of objects) you're tracking within an scroll parent, transforms are taken into account when calculating the position of the tracked element. This is probably what you want, but can be unexpected if you assumed offsetTop was used.

Track horizontal scrolling

Currently this library doesn't support horizontal scroll tracking. If this is something you need, please file an issue.


Disclaimer

This library is intended for internal use, we provide no support, use at your own risk. It does not import React, but expects it to be provided, which @kaliber/build can handle for you.

This library is not transpiled.