Frontend developer focused on inclusive design

Typing animation with Framer Motion and React

These are notes on how to create a text typing effect in a React-based (Next.js) application with Framer Motion library, including a link to the complete code example on GitHub.

Full code preview

Here is a full code of React component responsible for a text typing effect.

"use client";

/**
 * Importing necessary modules from their respective packages.
 *
 * `motion` is a component from the framer-motion library used to create animations.
 * `useMotionValue` is a hook from the framer-motion library that creates a motion value.
 * `useTransform` is a hook from the framer-motion library to create a new motion value by transforming another.
 * `animate` is a function from the framer-motion library to animate a motion value.
 */
import { motion, useMotionValue, useTransform, animate } from "framer-motion";

/**
 * Importing necessary hooks from React.
 *
 * `useEffect` is a hook that lets you perform side effects in your function components.
 * `useState` is a hook that lets you add state to your functional components.
 */
import { useEffect, useState } from "react";

// Type definition for the props expected by the AnimatedText component.
type AnimatedTextProps = {
  text: string; // The text to be animated.
};

/**
 * AnimatedText is a component that gradually reveals text from start to end, one character at a time.
 *
 * @param {string} text - The text to be animated.
 */
export default function AnimatedText({ text }: AnimatedTextProps) {
  // `count` is a motion value that starts at 0 and will animate up to the length of the text.
  const count = useMotionValue(0);

  // `rounded` is a transformed motion value of `count`, rounding it to the nearest whole number.
  const rounded = useTransform(count, (latest) => Math.round(latest));

  // `displayText` is a transformed motion value of `rounded`, slicing the text to the current count.
  const displayText = useTransform(rounded, (latest) => text.slice(0, latest));

  // `animationCompleted` is a state variable to keep track of whether the animation has completed.
  const [animationCompleted, setAnimationCompleted] = useState(false);

  useEffect(() => {
    /**
     * Initiating the animation of the `count` motion value from 0 to the length of the text.
     * The animation is linear over a 10 second duration.
     * An `onUpdate` callback is specified to check if the animation is complete, and if so, `setAnimationCompleted` is called with `true`.
     */
    const controls = animate(count, text.length, {
      type: "tween",
      duration: 10,
      ease: "linear",
      onUpdate: (latest) => {
        if (latest === text.length) {
          setAnimationCompleted(true);
        }
      },
    });

    // Returning a cleanup function to stop the animation when the component is unmounted.
    return controls.stop;
  }, []); // Empty dependency array means this useEffect will only run once, similar to `componentDidMount`.

  return (
    /**
     * Rendering a paragraph element with a class of `animation-completed` if the animation is complete,
     * otherwise, it renders with an empty class string.
     * Inside the paragraph, a `motion.span` element is rendered with the `displayText` motion value.
     */
    <p className={animationCompleted ? "animation-completed" : ""}>
      <motion.span>{displayText}</motion.span>
    </p>
  );
}

Code breakdown

Let’s breakdown the code presented above.

Import statements

import { motion, useMotionValue, useTransform, animate } from "framer-motion";
import { useEffect, useState } from "react";

In this section, modules are imported from the framer-motion library and the react library. The framer-motion library provides tools for creating animations, while react provides hooks for managing state and side effects.

  • motion: A component from the framer-motion library that aids in creating animations.
  • useMotionValue: A hook from framer-motion that creates a motion value, which is a value that can be animated.
  • useTransform: A hook from framer-motion used to create a new motion value by transforming another.
  • animate: A function from framer-motion to animate a motion value.
  • useEffect: A hook from react that lets you perform side effects in your function components.
  • useState: A hook from react that lets you add state to your functional components.

Type definition

type AnimatedTextProps = {
  text: string; // The text to be animated.
};

Here, a type definition is created for the props that the AnimatedText component expects to receive. It specifies that a text prop of type string is expected.

AnimatedText component

export default function AnimatedText({ text }: AnimatedTextProps) {
  //...
}

The AnimatedText component is defined as a default export, which accepts a single prop text of type string.

Motion values and transformed values

const count = useMotionValue(0);
const rounded = useTransform(count, (latest) => Math.round(latest));
const displayText = useTransform(rounded, (latest) => text.slice(0, latest));

Three motion values are created:

  1. count: Initializes at 0 and will animate up to the text length.
  2. rounded: Rounds count to the nearest whole number.
  3. displayText: Slices the text to display characters up to the current count.

State variables

const [animationCompleted, setAnimationCompleted] = useState(false);

A state variable animationCompleted is declared with an initial value of false to track the animation's completion.

useEffect Hook

useEffect(() => {
  const controls = animate(count, text.length, {
    type: "tween",
    duration: 10,
    ease: "linear",
    onUpdate: (latest) => {
      if (latest === text.length) {
        setAnimationCompleted(true);
      }
    },
  });

  return controls.stop;
}, []);

Within a useEffect hook, the animate function is invoked to animate the count motion value from 0 to the text length over a 10-second duration.

The onUpdate callback checks if the animation is complete, and if so, setAnimationCompleted is called with true.

A cleanup function is returned to stop the animation if the component is unmounted.

Rendering

return (
  <p className={animationCompleted ? "animation-completed" : ""}>
    <motion.span>{displayText}</motion.span>
  </p>
);

A paragraph element is rendered with a class of animation-completed if the animation is complete, otherwise, it renders with an empty class string.

Inside the paragraph, a motion.span element is rendered displaying the displayText motion value, revealing the text character by character as the count motion value animates.