15. Scroll: Progress
This example uses the useMotionValue()
and useTransform()
hooks to change the width of the bar at the bottom.
I explained these examples in more detail in Scroll Layers for Prototyping.
Code component
const items = [0, 1, 2, 3, 4]
const height = 70
const padding = 10
const size = 150
export default function CC_15_Scroll_Progress(props) {
const scrollY = useMotionValue(0)
const width = useTransform(
scrollY,
[0, -getHeight(items) + size],
["calc(0% - 0px)", "calc(100% - 40px)"]
)
return (
<div>
<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 (
<div
style={{
width: 150,
height: height,
borderRadius: 20,
backgroundColor: "#fff",
position: "absolute",
top: (height + padding) * index,
}}
/>
)
})}
</motion.div>
</motion.div>
<motion.div
style={{
width: width,
height: 6,
borderRadius: 3,
backgroundColor: "#fff",
position: "absolute",
bottom: 20,
left: 20,
}}
/>
</div>
)
}
function getHeight(items) {
const totalHeight = items.length * height
const totalPadding = (items.length - 1) * padding
const totalScroll = totalHeight + totalPadding
return totalScroll
}
Code overrides
Pro tip: Don’t use a ‘native’ scroll with overrides, or things will not work.
Version A
You can only use the useMotionValue()
hook inside an override (or code component). So I used motionValue()
here instead, outside of the overrides.
const scrollY = motionValue(0)
const contentHeight = 390
const scrollHeight = 150
const scrollDistance = -contentHeight + scrollHeight
const marginLeftAndRightOfBar = 40
export function Scroll(Component): ComponentType {
return (props) => {
return <Component {...props} contentOffsetY={scrollY} />
}
}
export function Progress_bar(Component): ComponentType<any> {
return (props) => {
const { style, ...rest } = props
const widthBar = useTransform(
scrollY,
[0, scrollDistance],
["calc(0% - 0px)", `calc(100% - ${marginLeftAndRightOfBar}px)`]
)
return <Component {...rest} style={{ ...style, width: widthBar }} />
}
}
Version B
The above version has a predefined contentHeight
(total of all the scrollable content) and scrollHeight
(height of the scrollable area). This version gets those numbers by giving the Scroll and its content a ref
and then reading their sizes through getBoundingClientRect()
.
const useStore = createStore({ scrollHeight: 0, contentHeight: 0 })
const scrollY = motionValue(0)
export function Scroll(Component): ComponentType {
return (props) => {
const [store, setStore] = useStore()
const ref = useRef(null)
useEffect(() => {
const rect = ref.current.getBoundingClientRect()
setStore({ scrollHeight: rect.height })
}, [])
return <Component {...props} contentOffsetY={scrollY} ref={ref} />
}
}
export function ScrollContent(Component): ComponentType {
return (props) => {
const [store, setStore] = useStore()
const ref = useRef(null)
useEffect(() => {
const rect = ref.current.getBoundingClientRect()
setStore({ contentHeight: rect.height })
}, [])
return <Component {...props} ref={ref} />
}
}
const marginLeftAndRightOfBar = 40
export function Progress_bar(Component): ComponentType<any> {
return (props) => {
const { style, ...rest } = props
const [store, setStore] = useStore()
const scrollDistance = -store.contentHeight + store.scrollHeight
const widthBar = useTransform(
scrollY,
[0, scrollDistance],
["calc(0% - 0px)", `calc(100% - ${marginLeftAndRightOfBar}px)`]
)
return <Component {...rest} style={{ ...style, width: widthBar }} />
}
}
15 comments on “15. Scroll: Progress”
Leave a Reply
You must be logged in to post a comment.
Ah, thanks! Will have a look.
Got it to work now, reapplying the override fixed it.
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?
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 beundefined
, 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
andbottom
distances.)That was it! Thanks 🙂
Hey Tes,
Really need your help here! Is there a way to reset an ongoing animation? I am in a weird situation — I have a progress bar that is animating to it’s 100% width, but in between I want this bar to jump at specific widths and continue from there to its original destination. How can I achieve this?
Scenario: Let’s say I have a page component with next and prev buttons. I have this progress bar that animates to 100% width and along the way it changes the pages automatically after say, every 4 seconds. But, in addition to the autoplay(which I am able to achieve), I want to tap on the next/prev buttons to change pages and also make the progress bar jump back and forth corresponding to the current page and restart the 4 seconds timer.
Thanks in advance!
This sounds very much like a Stories interface 😃
If I understand you correctly, the bar should switch directly to the new position (without animation), and from that point start the new 4-second animation.
Whenever you restart an animation it will use the same transition values (including
duration
) so that’s taken care of. Problem is that an animation will always start from a property’s current value. (So no jumping to a new start point.)There’s a way, though. There’s this
from
transition setting, which I’ve never used (I thought: “Why bother? We haveinitial
”), but this is exactly what it’s for.I did a quick proof of concept with a few overrides:
I hope that this is more or less what you’re looking for.
haha, only that my progress bar wants to be smarter and causing me pain 🙁
oh wow, never knew about “from” in transition. This definitely solves a large part of my scenario but in the expected behavior the bar never stops and thats what I was curios about. My main bar animation is going from 0-300 in 32 seconds. Every 4th second the next page loads automatically (I am able to achieve this). But when I tap on the next/prev buttons the pages change, the bar jumps to the corresponding position and autoplays from the new location to the destination of 300. The challenge is how to reset the 32 seconds while it is running.
I’ll try to build on top of what you shared but if you have anything right off your head to solve this scenario that will be super amazing!
For reference, here is what I was doing and achieving the autoplay behavior.
Ok so I did it! 🙂
Thanks for the help Tes! Really appreciate your quick response on that!!
You’re welcome! Nice solution with
onAnimationComplete()
.I’m struggling with a prototype that has text fading in and then out based on scroll position. I’m able to create a single effect using useTransform() but I’m not sure how to have it come from 20% opacity to 100% and back to 20%, all based on a scroll distance of ~500px.
You can use more than two values in
useTransform()
, so you can do something like:Tes, this was a huge help! I never knew this was the case, but it’s opened a ton of options for me. Thanks!