Reputation: 821
How can I animate a pure number from 0 to 100 with a transition config?
<motion.div>
{num}
</motion.div>
Upvotes: 11
Views: 18176
Reputation: 226
Thought I might add this in-case it's useful for anyone.
Differences compared to above answers
import { useEffect, useRef } from "react";
import { LazyMotion, domAnimation, m, useAnimate, useInView, useMotionValue, useTransform } from "framer-motion";
function ComponentName({ num }) {
const ref = useRef(null);
const isInView = useInView(ref, {
once: true,
margin: "0px 0px -100px 0px"
});
const [ _, animate ] = useAnimate();
const startingValue = useMotionValue(0);
const currentValue = useTransform(startingValue, (value) => (
Math.round(value).toLocaleString() + "+")
);
useEffect(() => {
if (isInView) {
animate(startingValue, num, {
duration: 5,
ease: "circIn"
});
}
}, [animate, isInView, num, startingValue]);
return (
<LazyMotion features={domAnimation}>
<m.p ref={ref}>{current}</m.p>
</LazyMotion>
);
}
Upvotes: 1
Reputation: 5902
With Framer Motion 2.8 a new animate
function was released, which is a good fit for this use case:
import { animate } from "framer-motion";
import React, { useEffect, useRef } from "react";
function Counter({ from, to }) {
const nodeRef = useRef();
useEffect(() => {
const node = nodeRef.current;
const controls = animate(from, to, {
duration: 1,
onUpdate(value) {
node.textContent = value.toFixed(2);
},
});
return () => controls.stop();
}, [from, to]);
return <p ref={nodeRef} />;
}
export default function App() {
return <Counter from={0} to={100} />;
}
Note that instead of updating a React state continuously, the DOM is patched manually in my solution to avoid reconciliation overhead.
See this CodeSandbox.
Upvotes: 34
Reputation: 320
The previous answer didn't work for me so I implemented this one here using the animate content part of this page https://www.framer.com/motion/animation/
import {motion, useMotionValue, useTransform, animate} from 'framer-motionn';
const Counter = ({ from, to, duration }) => {
const count = useMotionValue(from);
const rounded = useTransform(count, (latest) => Math.round(latest));
useEffect(() => {
const controls = animate(count, to, { duration: duration });
return controls.stop;
}, []);
return <motion.p>{rounded}</motion.p>;
};
Upvotes: 1
Reputation: 831
For those seeking a TypeScript-based approach, including a in view execution.
import { animate } from "framer-motion";
import React, { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
interface CounterProps {
from: number;
to: number;
}
const Counter: React.FC<CounterProps> = ({ from, to }) => {
const nodeRef = useRef<HTMLParagraphElement | null>(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const node = nodeRef.current;
if (!node) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInView(true);
}
});
},
{ threshold: 0.1 }
);
observer.observe(node);
return () => {
observer.unobserve(node);
};
}, []);
useEffect(() => {
if (!isInView) return;
const node = nodeRef.current;
if (!node) return;
const controls = animate(from, to, {
duration: 1,
onUpdate(value) {
node.textContent = Math.round(value).toString();
},
});
return () => controls.stop();
}, [from, to, isInView]);
return (
<motion.p
ref={nodeRef}
initial={{ opacity: 0, scale: 0.1 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.4 }}
/>
);
};
export default Counter;
Upvotes: 1