<- GOBACK

Can we replace framer-motion with scroll timelines?

Last week I was playing with css animation timelines. They have the power to simplify web animations, but are they enough?

When we have full browser support this will be a welcome new tool in our toolbox, however they seem to come with some caveats I don't know how to solve with the current api design. Let's see if I can rewrite the "Epic screenshot", a scroll powered animation we made for the recent redesign of Flare. You should go check out the animation in action on a blog post I wrote about the current implementation on the Flare blog first.

Here is the main implementation written using framer motion.

import { motion, useScroll, useSpring, useTransform } from "framer-motion";

fuction FlyInContainer({ children }) {
	const ref = React.useRef(null);
	const { scrollYProgress } = useScroll({
		target: ref,
		offset: ["start start", "start end"]
	});
	
	const rotateX = useTransform(scrollYProgress, [1, 0.6], [60, 0]);
	const z = useTransform(scrollYProgress, [1, 0.6], [1000, 0]);

	const filter = useTransform(scrollYProgress, [1, 0.6], ["blur(20px)", "blur(0px)"]);
	
	return (
		<div style={{ perspective: 1000 }}>
			<motion.div
				ref={ref}
				className="origin-bottom"
				style={{ rotateX, z, filter }}
			>
				{children}
			</motion.div>
		</div>
	);	
}

After playing around with scroll timelines in another project, it took me only a minute to transfer this effect to css.

The steps I took to get here were pretty straightforward.

1. Apply the animated properties

The 3D transforms and filter effect are almost 1 to 1 rewrites.

.fly-in-container {
	--progress: 1; /* scrollYProgress, we will animate this later */
	
	rotate: x calc(var(--progress) * 60deg); /* useTransform(scrollYProgress, [1, 0.6], [60, 0]) */
	translate: 0 0 calc(var(--progress) * 1000px); /* useTransform(scrollYProgress, [1, 0.6], [1000, 0]) */
	filter: blur(calc(var(--progress) * 20px)); /* useTransform(scrollYProgress, [1, 0.6], ["blur(20px)", "blur(0px)"]) */
}

2. Setup the timeline.

Since all of the properties have the same interpolation base and start and stop points, we can define the endpoints as keyframe offsets and animate just one progress property

@keyframes reverse-progress-0-60 {
	from { --progress: 1 }
	60%, to { --progress: 0 }
}

3. Register a css custom property

By registering the property as a number, css knows that it is animatable.

@property --progress {
	syntax: "<number>";
	initial-value: 1;
	inherits: true;
}

4. Bind the timeline to scroll

The css scroll function defaults are nearest scrollable parent and in the block scrolling direction. These defaults are exactly what we want here.

.fly-in-container {
	animation-name: reverse-progress-0-60;
	animation-timeline: scroll();
}

5. Remove framer motion

All that is left is to remove all of the framer motion logic from the react component and this is the resulting code.

import './styles.css';

fuction FlyInContainer({ children }) {
	return (
		<div style={{ perspective: 1000 }}>
			<div className="fly-in-container | origin-bottom">
				{children}
			</div>
		</div>
	);	
}
@property --progress {
	syntax: "<number>";
	initial-value: 1;
	inherits: true;
}

.fly-in-container {
	animation-name: reverse-progress-0-60;
	animation-timeline: scroll();
	
	rotate: x calc(var(--progress) * 60deg);
	translate: 0 0 calc(var(--progress) * 1000px);
	filter: blur(calc(var(--progress) * 20px));
}

@keyframes reverse-progress-0-60 {
	from { --progress: 1 }
	60%, to { --progress: 0 }
}

I would rather maintain this css snippet than the previous custom framer motion code.