The useSpring() animations run even without rendering, and api.start() can't change keyframes during a loop. We create a new useSpring() hook to change the style keyframe.
Changing useSpring() on the same loop element
We useState() style the useSprings(), alternating them with useRef().
The new useSpring() won't startfrom:{} but from its already running keyframe.
//It will render depending on the number of keyframes and duration//It renders on the DOM so we useState()let [spin, api] =useSpring(()=>({ from: {x:0, y:0, background:"lightblue"}, to: [ {x:100, background:"lightblue"}, {y:80, background:"blue"}, {x:0, background:"lightgreen"}, {y:0, background:"green"}, ], loop:true, config:config.wobbly}))let [spin1, api1] =useSpring(()=>({ from: {x:0, y:0, background:"pink"}, to: [ {x:100, background:"pink"}, {y:80, background:"orange"}, {x:0, background:"red"}, {y:0, background:"orange"}, ], loop:true, config:{ duration:1000 }}))let [stile, setStile] =useState(spin)let trigger2 =useRef(false)functioncambio1(){trigger2.current ?setStile(spin) :setStile(spin1)trigger2.current =!trigger2.current}<divclassName="d-block"> <animated.divclassName="boxo"style={stile}> </animated.div> <divclassName="text-center"> <buttonclassName="btn btn-primary"onClick={cambio1}>Change</button> </div></div>
We can pause() and resume() to keep the useSpring() keyframe when changed.
Changing useSpring() while pause() resume() keyframes
We useEffect() to keep the second useSpring() paused before resuming and rendering it.
//useEffect() works once onLoad(), then we pause() resume() the useSpring()let [spin2, api2] =useSpring(()=>({ from: {x:0, y:0, background:"lightblue"}, to: [ {x:100, background:"lightblue"}, {y:80, background:"blue"}, {x:0, background:"lightgreen"}, {y:0, background:"green"}, ], loop:true, config:config.wobbly}))let [spin3, api3] =useSpring(()=>({ from: {x:0, y:0, background:"pink"}, to: [ {x:100, background:"pink"}, {y:80, background:"orange"}, {x:0, background:"red"}, {y:0, background:"orange"}, ], loop:true, config:{ duration:1000 }}))let [stile1, setStile1] =useState(spin2)let trigger3 =useRef(false)let fermo1 =trueuseEffect(()=>{api3.pause()}, [fermo1])functioncambio2(){if(trigger3.current){setStile1(spin2)api2.resume()api3.pause() }else{setStile1(spin3)api3.resume()api2.pause() }trigger3.current =!trigger3.current}<divclassName="d-block"> <animated.divclassName="boxo"style={stile1}> </animated.div> <buttonclassName="btn btn-primary"onClick={cambio2}> Resume </button></div>
Sequential New SpringValue() properties
We new SpringValuesingle style properties, to sequentially animate them as keyframes we async/await.
//We can keep multiple in an object and it's similar to useSpringValue()import { SpringValue } from'@react-spring/web'constmossa1= { x:newSpringValue("0%", {config: {duration:500} }), y:newSpringValue( 0, {config: {duration:500}} ),}asyncfunctionprimo(){awaitmossa1.x.start("50%")awaitmossa1.y.start(100)mossa1.x.set("0%")mossa1.y.set(0)primo()}<div> <animated.divclassName="boxo"style={{ marginLeft:mossa1.x, y:mossa1.y }} > </animated.div></div>
Sequential SpringValue objects on re-set loop
We sequentially animate the SpringValue(), reset it to its starting value, and re-start its function.
The new Controller class constructor sets an api imperative to springValues.
//Controller can use springValues() methods, controller..springs for style values
//It works the same as a useSpring() api
import {Controller} from '@react-spring/web'
let mover = new Controller({
x: 0,
y: 0,
onRest: () => {
mover.each((springValue, key) => {
console.log(`Spring key: ${key}, Value: ${springValue.get()}`);
});
},
})
<div>
<animated.div className="round" style={mover.springs}>
</animated.div>
</div>
The each() method triggers once for each style property.
Bounce controller animation with springValue() functions
No function triggers on bounce, we use an onChange() to trigger a limited number of horizontal animations, with a different timing function from the controller.
We reset() the controller springValues and start() with randomized tension/friction.
//onChange() doesn't trigger on single pixel of animation, we need a range
let rimba = new Controller({
x: 0,
y: 0,
config: {
mass: 2,
tension: 20,
friction: 15,
bounce: 1.5,
},
onChange: (x) => {
if( x.value.y < 275 && mossa.current <= 100 ){
mossa.current += 1
rimba.start({
x: mossa.current * 2,
config:{ mass: 1, tension: 170, friction: 26, bounce: 0 }
})
}
},
})
function starto(){
let tenso = Math.round(350 - (Math.random() * 100))
let fricto = Math.round(35 - (Math.random() * 20))
rimba.set({x: 0, y: 0})
mossa.current = 0
rimba.start({
y: 300,
config:{
tension: tenso,
friction: fricto
}
})
}
<div>
<animated.div className="round ms-1" style={rimba.springs}>
</animated.div>
<button className="btn btn-primary" onClick={starto}>
start
</button>
</div>
Bounce and horizontal animation
useSpring() scrips on conditional keyframes
We chain useSpring() keyframes with an array of spring objects, but we can't change the keyframe array during the animation (unless we change the rendered useSpring()). So we create a script.
An async function that await the next (function that returns a Promise on the spring state) once completed.
//onStart() triggers on each keyframe and you don't need commas on next({})
let [circle, circleapi] = useSpring(()=>({
from: {x: 0, y: 0, backgroundColor: "green"},
to: [
{x: 100, y: 0, backgroundColor: "yellow"},
{x: 50, y: 100, backgroundColor: "red"}
]
}))
let [ruota, ruotaapi] = useSpring(()=>({
from: {x: 0, y: 0, backgroundColor: "green"},
to: async (next, cancel) => {
await next({x: 100, y: 0, backgroundColor: "yellow"})
await next({x: 50, y: 100, backgroundColor: "red" })
}
}))
Conditional keyframes and re-started loop animations
We useRef() to trigger a different set of keyframes in the script.
We reset the next keyframe with the ternary operator, making it transparent and positioning it at the beginning (from) using a different config() while restarting the loop with different keyframes.
Switching multiple useSpring() on the same element
useIsomorphicLayoutEffect() and useReducedMotion()
The React hooks useEffect() and useLayout() affect the server and the client respectively.
We properly render useSpring() effects with useIsomorphicLayoutEffect(), using a useState() todependency-animate useSpring() properties.
//We can't insert a loop:true in a start() or a single value useSpring()
let [dove, setDove] = useState({x: 0, y: -100})
let [primo, apiPrimo] = useSpring(() =>({
x: 0, y: 0
}))
useIsomorphicLayoutEffect(()=>{
apiPrimo.start({
x: dove.x, y: dove.y
})
}, [dove])
let [secondo, apiSecondo] = useSpring(() =>({
from: {x: 0, y: 0},
to: {x: 0, y: -100},
}))
function mossa(){
apiSecondo.start({ x: 0, y: 100 })
setDove({x: 0, y: 100})
}
<div>
<animated.div className="boxo" style={primo} />
<animated.div className="boxo" style={secondo} />
<button className="btn btn-primary" onClick={mossa}> Starto </button>
</div>
The useReducedMotion() hook will trigger if the user lowers animations in its device.
//We use the boolean returned value
const reducedMotion = useReducedMotion()
<div>
<h1>Hello World</h1>
{reducedMotion ? <p>You're using reduced motion!</p> : null}
</div>
Animation useSpring() values onScroll()
The useScroll() is a utility abstraction used to create scroll-linked animations.
It returns the scrollY springValue, the pixel scroll distance, and scrollYProgress, a 0-1 value relative to the entire useRef() container. Both are built-in so we have to rename them with : if we have multiple useScroll().
We can animate an useSpring() on the useScroll() onChange() or interpolate it with a ternary operator.
useSpring() with interpolate or onChange() onScroll() animation
Animated Math.cos() wave onScroll()
We loop a new Array().fill() to render multiple 100% viewpoint DOM elements, for the onScroll() to take place.
//The created array of empty values is used only for the index
const PAGE_COUNT = 5
<div>
{new Array(PAGE_COUNT).fill(null).map((_, index) => (
<div style={{ width: "100vw", height: "100vh" }} key={index} />
))}
</div>
We interpolate the circle clipPath() area and onScroll() onChange() to animate the translateY(0-> 100%) text.
We Array.from() render fixed bands on both sides of the window, and we onScroll() scrollYProgress interpolate their width style prop.
Each bar will have a fixed scrollP (their current scroll position) and fixed percentileP (their singular index dependantpercentage current position on the total height, 0.025 for the first and 1 for the last).
//Array.from({length: x}) is no different from new Array(x).fill
//we add 1 to the 0 index array.from().map array
//The last bar will have percentileP 1 (by itself) and scrollP 0->1 (shared)
const X_LINES = 40
<div className="animated__layers">
<animated.div className="bar__container">
{Array.from({ length: X_LINES }).map((_, i) => (
<animated.div key={i} className="bar"
style={{
width: scrollYProgress.to(scrollP => {
const percentileP = (i + 1) / X_LINES
return inter(percentileP, scrollP)
}),
}}
/>
))}
</animated.div>
<animated.div className="bar__container__inverted">
</animated.div>
</div>
We use Math.cos() to start the wave at percentileP 0.025. We multiply the parameters' difference (onScroll() responsive) inside Math.cos() to increase the difference between bars and then the result by 40 (max-width)
//We Math.pi() to use radians, the width is set at 5 if it is too small
//To not get double waves we check the internal multiplier with bar number
//The parameters' difference is always -Math.pi()< x < Math.pi()
function inter(percentileP scrollP){
let y = (Math.cos( (percentileP - scrollP) * Math.PI * 1.5 ) * 40)
let y1 = (y < 5) ? y = 5 : null
return y
}
The flex-column sidebar container is fixed, we changed the align-items for the left reverse bars.