8

How can I animate a pure number from 0 to 100 with a transition config?

<motion.div>
  {num}
</motion.div>
amann
  • 5,449
  • 4
  • 38
  • 46
Yokiijay
  • 711
  • 2
  • 7
  • 18

3 Answers3

29

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.

Sibiraj
  • 4,486
  • 7
  • 33
  • 57
amann
  • 5,449
  • 4
  • 38
  • 46
  • Update: **this will add to your bundle size**. Framer motion has now removed the popmotion dependency so you will need to install + load popmotion seperately – Downgoat Sep 23 '20 at 07:32
  • Isn't the dependency still there? https://github.com/framer/motion/blob/b3c8d88d3b689651f34ba9c266aaf398a6e3b1d2/package.json#L97 It's worth noting though that you should use the transitive dependency, e.g. if you `npm install popmotion` you likely end up with two copies of popmotion in your bundle. – amann Sep 23 '20 at 08:22
  • sorry I misread the framer motion change log : you're correct popmotion is already listed as a dependency, but framer-motion only imports a list of key-value pairs and utility functions,everything else is tree-shaked out. If you wish to use popmotion you should still explicitly install it and understand that there will be additional kb being added to ur bundle from the import – Downgoat Sep 23 '20 at 08:41
  • 1
    @Downgoat Framer Motion 2.8 is out with a newly added utility function for this use case. I've updated my solution accordingly. – amann Oct 09 '20 at 09:15
  • To an extension to this question, can this API be used with variants, such as `staggerChildren` etc? – Daniel Cheung Jun 28 '21 at 15:45
1

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;
Sarotobi
  • 707
  • 1
  • 9
  • 28
1

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>;
};
Joeboulton
  • 138
  • 8