Framer X » Animation » Example animations » 18. Scroll: Progress

18. Scroll: Progress

This example uses the useMotionValue() and useTransform() hooks to change the width of the bar at the bottom.

Open Framer Motion version in CodeSandbox

Code Component

const items = [1, 2, 3, 4, 5, 6]
const height = 70
const padding = 10
const scrollSize = 150

export function CC18ScrollProgress() {
    const itemsHeightTotal = items.length * height
    const totalPadding = (items.length - 1) * padding
    const contentHeight = itemsHeightTotal + totalPadding

    const scrollDistance = -contentHeight + scrollSize

    const scrollY = useMotionValue(0)
    const width = useTransform(
        scrollY,
        [0, scrollDistance],
        ["calc(0% - 0px)", "calc(100% - 40px)"]
    )

    return (
        <div>
            <Scroll
                // Visual & layout
                width={150}
                height={150}
                radius={30}
                center
                // Scrolling
                contentOffsetY={scrollY}
            >
                {items.map(index => {
                    return (
                        <Frame
                            // Visual & layout
                            width={scrollSize}
                            height={height}
                            radius={20}
                            backgroundColor="#fff"
                            top={(height + padding) * (index - 1)}
                            // Required by React
                            key={index}
                        />
                    )
                })}
            </Scroll>
            <Frame
                width={width}
                height={6}
                radius={3}
                backgroundColor="#fff"
                bottom={20}
                left={20}
            />
        </div>
    )
}

Framer Motion

const items = [0, 1, 2, 3, 4]
const height = 70
const padding = 10
const size = 150

export function FM18ScrollProgress() {
    const scrollY = useMotionValue(0)

    const width = useTransform(
        scrollY,
        [0, -getHeight(items) + size],
        ["calc(0% - 0px)", "calc(100% - 40px)"]
    )

    return (
        <Center>
            <motion.div
                style={{
                    width: 150,
                    height: 150,
                    borderRadius: 30,
                    overflow: "hidden",
                    position: "relative",
                    transform: "translateZ(0)",
                    cursor: "grab",
                }}
                whileTap={{ cursor: "grabbing" }}
            >
                <motion.div
                    style={{
                        width: 150,
                        height: getHeight(items),
                        y: scrollY,
                    }}
                    drag="y"
                    dragConstraints={{
                        top: -getHeight(items) + size,
                        bottom: 0,
                    }}
                >
                    {items.map(index => {
                        return (
                            <motion.div
                                style={{
                                    width: 150,
                                    height: height,
                                    borderRadius: 20,
                                    backgroundColor: "#fff",
                                    position: "absolute",
                                    top: (height + padding) * index,
                                }}
                                key={index}
                            />
                        )
                    })}
                </motion.div>
            </motion.div>
            <motion.div
                style={{
                    width: width,
                    height: 6,
                    borderRadius: 3,
                    backgroundColor: "#fff",
                    position: "absolute",
                    bottom: 20,
                    left: 20,
                }}
            />
        </Center>
    )
}

function getHeight(items) {
    const totalHeight = items.length * height
    const totalPadding = (items.length - 1) * padding
    const totalScroll = totalHeight + totalPadding
    return totalScroll
}

Overrides

const appState = Data({
    widthBar: "calc(0% - 0px)",
})

export function Progress_bar(): Override {
    return {
        width: appState.widthBar,
    }
}

export function Scroll(props): Override {
    const contentHeight = props.children[0].props.children[0].props.height
    const scrollSize = props.height as number
    const scrollDistance = -contentHeight + scrollSize

    const scrollY = useMotionValue(0)
    const width = useTransform(
        scrollY,
        [0, scrollDistance],
        ["calc(0% - 40px)", "calc(100% - 40px)"]
    )
    return {
        contentOffsetY: scrollY,
        onScroll() {
            appState.widthBar = width.get()
        },
    }
}

8 comments on “18. Scroll: Progress”

  • Espen Staver says:

    Will this override example work with a scroll component made with the scroll tool on the canvas? Having some difficulties…can’t seem to get to the height values this way?

    • Tes Mat says:

      Yes, the Overrides version works with a Scroll from the Canvas. Check the file with all these examples on: https://framerbook.com/x/animation/example-animations/

      But this example is rather complicated because it also reads the height of the scroll content in a hacky way (by digging into the props of the children)

      I found a simpler ‘scroll progress’ example (with hard coded values) in the Framer Web beta:

      import { Override, motionValue, useTransform } from "framer"
      
      // Track the scroll amount with a motion value
      const scrollY = motionValue(0)
      
      export function Scroll(): Override {
          return {
              contentOffsetY: scrollY,
          }
      }
      
      export function Progress(): Override {
          // Animated width in relation to the scrolled amount
          const width = useTransform(scrollY, [0, -150], [0, 90])
      
          return {
              width: width,
          }
      }
      • Espen Staver says:

        Ah, thanks! Will have a look.

        • Espen Staver says:

          I am still a little puzzled by this. If i copy your example 1:1 in a new framer document, it still does not work. In your file, the props.children[0].props.children[0].props.height yields the height, however in my copy it comes up as “undefined”?

          • Espen Staver says:

            Got it to work now, reapplying the override fixed it.

          • Espen Staver says:

            Hm, that wasn’t it. There is certainly something quirky going on: When I place the scroll component further down in the container frame, I can get a read from props.height for example. However if the scroll component is placed with no margin at the top , I get an “undefined”. What could be causing this?

          • Tes Mat says:

            Found it! (I hope.)

            Check the how the scroll component is pinned. When it’s pinned to both the top and bottom its props.height will be undefined, and that’s when things break down.

            (It makes sense that height is undefined in that case, because the actual height will then be calculated from the top and bottom distances.)

          • Espen Staver says:

            That was it! Thanks 🙂

Leave a Reply