Skip to content

Instantly share code, notes, and snippets.

@perrysmotors
Last active June 20, 2024 21:07
Show Gist options
  • Save perrysmotors/9622d1aa0be45fef3a266d305cb4c975 to your computer and use it in GitHub Desktop.
Save perrysmotors/9622d1aa0be45fef3a266d305cb4c975 to your computer and use it in GitHub Desktop.
Overrides to create scroll interactions on Framer sites
import type { ComponentType } from "react"
import { useState, useEffect } from "react"
import type { MotionValue, Transition } from "framer-motion"
import {
useScroll,
useVelocity,
useTransform,
useMotionValue,
animate,
} from "framer-motion"
// The following overrides are for creating scroll effects on web pages
export function withParallax(Component): ComponentType {
const speed = 1
return (props: any) => {
const { scrollY } = useScroll()
const x = useTransform(scrollY, (value) => -value * speed) // scrolling down translates left
return <Component {...props} style={{ ...props.style, x }} />
}
}
// Scrub through a video or drive a Lottie animation by scrolling
export function withScrolledProgress(Component): ComponentType {
const startY = 0 // scroll position when animation starts
const distance = 1000 // scroll distance after which animation ends
const endY = startY + distance
return (props) => {
const { scrollY } = useScroll()
const progress = useTransform(scrollY, [startY, endY], [0, 1])
return <Component {...props} progress={progress} />
}
}
export function withScrollLinkedValue(Component): ComponentType {
// Value being driven by scrolling (e.g. height)
const initialValue = 200
const finalValue = 100
const speed = 1
const scrollDistance = (initialValue - finalValue) / speed
const startY = 150 // scroll position when transition starts
const endY = startY + scrollDistance
return (props: any) => {
const { scrollY } = useScroll()
const scrollOutput = useTransform(
scrollY,
[startY, startY, endY, endY],
[initialValue, initialValue, finalValue, finalValue],
{
clamp: false,
}
)
return <Component {...props} style={{ ...props.style, height: scrollOutput }} />
}
}
export function withScrollToggledVariant(Component): ComponentType {
const thresholdY = 500 // set the scroll position where you want the component to switch
return (props) => {
const { scrollY } = useScroll()
const [isPastThreshold, setIsPastThreshold] = useState(false)
useEffect(
() =>
scrollY.onChange((latest) =>
setIsPastThreshold(latest > thresholdY)
),
[]
)
return (
<Component
{...props}
variant={isPastThreshold ? "Second" : "First"} // variants to animate between
/>
)
}
}
export function withSlideOutOnScrollUp(Component): ComponentType {
const slideDistance = 100 // if we are sliding out a nav bar at the top of the screen, this will be it's height
const threshold = 500 // only slide it back when scrolling back at velocity above this positive (or zero) value
return (props) => {
const { scrollY } = useScroll()
const scrollVelocity = useVelocity(scrollY)
const [isScrollingBack, setIsScrollingBack] = useState(false)
const [isAtTop, setIsAtTop] = useState(true) // true if the page is not scrolled or fully scrolled back
const [isInView, setIsInView] = useState(true)
useEffect(
() =>
scrollVelocity.onChange((latest) => {
if (latest > 0) {
setIsScrollingBack(false)
return
}
if (latest < -threshold) {
setIsScrollingBack(true)
return
}
}),
[]
)
useEffect(
() => scrollY.onChange((latest) => setIsAtTop(latest <= 0)),
[]
)
useEffect(
() => setIsInView(isScrollingBack || isAtTop),
[isScrollingBack, isAtTop]
)
return (
<Component
{...props}
animate={{ y: isInView ? 0 : -slideDistance }}
transition={{ duration: 0.2, delay: 0.25, ease: "easeInOut" }}
/>
)
}
}
export function withScrollTriggeredStates(Component): ComponentType {
const scrollYRange = [0, 1000, 1600] // scroll positions that trigger the animation
const outputRange = ["First", "Second", "Third"] // list of variants to animate between
return (props) => {
const state = useScrollTriggeredState(scrollYRange, outputRange)
return <Component {...props} variant={state} />
}
}
// Trigger a state change when each layer with a <section> tag reaches the top of the page
// You can apply a <section> tag to a layer through the 'Accessibility' property controls
export function withSectionTriggeredStates(Component): ComponentType {
const outputRange = ["First", "Second", "Third"] // list of variants to animate between
return (props) => {
const { scrollY } = useScroll()
const [state, setState] = useState(outputRange[0])
useEffect(() => {
const scrollYRange = getSectionPositions()
scrollY.onChange((latest) => {
const output = getCorrespondingItem(
latest,
scrollYRange,
outputRange
)
setState(output)
})
}, [])
return <Component {...props} variant={state} />
}
}
export function withScrollTriggeredAnimation(Component): ComponentType {
const scrollYRange = [0, 1000, 1600] // scroll positions that trigger the animation
const outputRange = ["#8E47BA", "#000AFF", "#FF0000"] // list of values to animate to
// customise the transition
const transition: Transition = {
type: "tween",
duration: 1,
ease: "easeInOut",
}
return (props: any) => {
const animatedValue = useMotionValue(outputRange[0])
const { scrollY } = useScroll()
const scrollOutput = useSteppedTransform(
scrollY,
scrollYRange,
outputRange
)
useEffect(
() =>
scrollOutput.onChange(
(latest) => animate(animatedValue, latest, transition) // remove transition to use default
),
[]
)
return (
<Component {...props} style={{ ...props.style, backgroundColor: animatedValue }} /> // override value you want to animate
)
}
}
// Trigger an animation when each layer with a <section> tag reaches the top of the page
// You can apply a <section> tag to a layer through the 'Accessibility' property controls
export function withSectionTriggeredAnimation(Component): ComponentType {
const outputRange = ["#FFEE66", "#000AFF", "#FF0000"] // list of values to animate to
// customise the transition
const transition: Transition = {
type: "tween",
duration: 1,
ease: "easeInOut",
}
return (props: any) => {
const animatedValue = useMotionValue(outputRange[0])
const handleSectionChange = (latest) =>
animate(animatedValue, outputRange[latest], transition) // remove transition to use default
useSectionTrigger(handleSectionChange)
return (
<Component {...props} style={{ ...props.style, backgroundColor: animatedValue }} /> // override value you want to animate
)
}
}
// Apply the current scroll target to the URL displayed in the web browser
// You can apply a scroll target to a layer through the 'Scroll Target' property controls
export function withScrollTargetHistory(Component): ComponentType {
return (props) => {
const { scrollY } = useScroll()
const scrollOutput = useMotionValue("#")
const handleTargetChange = (latest) =>
history.replaceState(null, "", latest)
useEffect(() => {
const { scrollYRange, outputRange } = getScrollTargets()
scrollY.onChange((latest) => {
const index = getMatchingIndex(latest, scrollYRange)
if (scrollOutput.get() !== outputRange[index]) {
scrollOutput.set(outputRange[index])
}
})
}, [])
useEffect(() => scrollOutput.onChange(handleTargetChange), [])
return <Component {...props} />
}
}
// Custom hooks
function useSteppedTransform(
value: MotionValue,
inputRange: number[],
outputRange: any[]
) {
return useTransform(value, (value) =>
getCorrespondingItem(value, inputRange, outputRange)
)
}
function useScrollTriggeredState(inputRange: number[], outputRange: any[]) {
const { scrollY } = useScroll()
const [state, setState] = useState(outputRange[0])
useEffect(
() =>
scrollY.onChange((latest) =>
setState(getCorrespondingItem(latest, inputRange, outputRange))
),
[]
)
return state
}
function useSectionTrigger(handleSectionChange) {
const scrollOutput = useMotionValue(0)
const { scrollY } = useScroll()
useEffect(() => {
const scrollYRange = getSectionPositions()
scrollY.onChange((latest) => {
const index = getMatchingIndex(latest, scrollYRange)
if (scrollOutput.get() !== index) {
scrollOutput.set(index)
}
})
}, [])
useEffect(() => scrollOutput.onChange(handleSectionChange), [])
}
// Functions
function getMatchingIndex(value, array) {
let found = array.findIndex((el) => el > value)
switch (found) {
case 0:
return 0
break
case -1:
return array.length - 1
break
default:
return found - 1
}
}
function getCorrespondingItem(
value: number,
inputRange: number[],
outputRange: any[]
) {
const inputIndex = getMatchingIndex(value, inputRange)
const outputIndex =
inputIndex > outputRange.length - 1
? outputRange.length - 1
: inputIndex
return outputRange[outputIndex]
}
function getSectionPositions() {
const elements = Array.from(document.querySelectorAll("section"))
const positions = elements
.map((element) => {
return element.getBoundingClientRect().top + window.scrollY
})
.sort((a, b) => a - b)
if (positions[0] === 0) {
return positions
} else {
return [0, ...positions]
}
}
function getScrollTargets() {
const elements = Array.from(document.querySelectorAll('[id]:not([id=""])'))
const targets = elements
.map((element) => {
return {
y: element.getBoundingClientRect().top + window.scrollY,
target: `#${element.id}`,
}
})
.sort((a, b) => a.y - b.y)
const inputs = targets.map((target) => target.y)
const outputs = targets.map((target) => target.target)
if (inputs[0] === 0) {
outputs[0] = "#"
return { scrollYRange: inputs, outputRange: outputs }
} else {
return {
scrollYRange: [0, ...inputs],
outputRange: ["#", ...outputs],
}
}
}
@protomuse
Copy link

Big up PerrysMotors ;-)

@CodePatrolOPG
Copy link

Is it possible to set inView trigger to these functions?

@perrysmotors
Copy link
Author

Is it possible to set inView trigger to these functions?

import type { ComponentType } from "react"
import { createRef, useEffect, useRef } from "react"
import { useScroll, useTransform, useSpring } from "framer-motion"

// Scrub through a video or drive a Lottie animation by scrolling while it is in view
export function withScrolledProgressInView(Component): ComponentType {
    return (props: any) => {
        const ref = useRef(null)

        const { scrollYProgress } = useScroll({
            target: ref,
            offset: ["end", "start"],
            layoutEffect: false, // fix required for sticky elements
        })

        return (
            <div ref={ref} style={props.style}>
                <Component {...props} progress={scrollYProgress} />
            </div>
        )
    }
}

@shamsdigital
Copy link

Man! you are great!

@pizza0502
Copy link

Is it possible to change the scroll direction of a element that is set overflow:scroll?
I have a super wide horizontal art works I want to show in full screen, and using vertical scrolling to view from left to right...

@perrysmotors
Copy link
Author

perrysmotors commented Dec 20, 2023

Is it possible to change the scroll direction of a element that is set overflow:scroll? I have a super wide horizontal art works I want to show in full screen, and using vertical scrolling to view from left to right...

@pizza0502 withParallax() can be used to translate an element horizontally when you scroll vertically. If the position of the element is 'fixed' then the element won't scroll vertically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment