
May 2, 2024
•
#react
#animations
•
2 mins read
In this blog post, we'll guide you through building a reusable counter animation component using Framer Motion in React. This animation can be a powerful tool for showcasing metrics on your website, like total revenue, positive reviews, or completed projects, in a way that grabs attention.
Following DRY (Don't Repeat Yourself) principles, we'll create a reusable component named AnimatedCounter.tsx to handle the animation logic. This component will accept props to customize the animation and display the counter.
Here's the basic structure of the component:
type AnimatedCounterProps = {
from: number;
to: number;
animationOptions?: KeyframeOptions;
};
function AnimatedCounter({
from = 0,
to,
animationOptions,
}: AnimatedCounterProps) {
return <span />;
}
export default AnimatedCounter;As you can see, the component receives a starting value (from), an ending value (to), and optional animation options (animationOptions). It then returns a simple span element. We'll discuss the reason behind this seemingly empty element later.
To create the animation, we'll use Framer Motion's imperative approach. Here's what's involved:
span within our code.useIsomorphicLayoutEffect for server-side rendering).animate function from Framer Motion brings the numbers to life with the animation.animate function, we'll use the onUpdate event to update the displayed value as the animation progresses, creating the count-up effect.The complete code for the AnimatedCounter component looks like this:
"use client";
import {
KeyframeOptions,
animate,
useInView,
useIsomorphicLayoutEffect,
} from "framer-motion";
import { useRef } from "react";
type AnimatedCounterProps = {
from: number;
to: number;
animationOptions?: KeyframeOptions;
};
function AnimatedCounter({
from = 0,
to,
animationOptions,
}: AnimatedCounterProps) {
// Create a ref for our span element
const ref = useRef<HTMLSpanElement>(null);
// Fire animation when in viewport (but only once)
const inView = useInView(ref, { once: true });
useIsomorphicLayoutEffect(() => {
const element = ref.current;
if (!element) return;
if (!inView) return; // Don't fire the animation if outside the viewport
const controls = animate(from, to, {
// Default animation options
duration: 1.2,
ease: "easeOut",
// Overrides, if passed through props
...animationOptions,
// Continiously set a new number while animating
onUpdate(value) {
element.textContent = value.toFixed(0);
},
});
return () => controls.stop();
}, [from, to, inView]); // If props are changed, restart the animation
return <span ref={ref} />;
}
export default AnimatedCounter;
While our animation works great, it might not be ideal for users who have enabled reduced motion settings on their devices. To ensure a good user experience, we can implement this check in two ways:
prefers-reduced-motion media query within the useLayoutEffect hook. If reduced motion is enabled, we simply display the final counter value without animation.Both approaches achieve the same result. Choose the one that best suits your project structure.
This concludes the tutorial! With this technique, you can now create beautiful and engaging counter animations to enhance your website's storytelling. Go forth and impress your visitors!