# How I Created a Typing Text Animation with Framer Motion

## The Idea

After finishing some client work, just like everyone else and their grandma, I made an AI app 🙄. Mine writes custom cover letters based on the user's CV and a job description.

When I got around to making the homepage, I knew I wanted to make an emotional impact with my hero section. Something relatable that reminds the user of the frustration of writing cover letters themselves. I came up with this. To see the animation in action, visit [covercraft.io](http://covercraft.io).

<iframe src="https://codesandbox.io/embed/text-typing-animation-rtzvgl?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fcomponents%2FA4Layout.tsx&theme=dark" style="width:100%;height:500px;border:0;border-radius:4px;overflow:hidden" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>

The section on the right looks like a Google Doc. It shows how someone is trying to write a cover letter but failing to find the right words. They re-write the first sentence repeatedly, but every attempt is more cringe-worthy than the last.

In this article, I will show how I made this text typing effect with framer motion. I will use a lot of different animation techniques. If you are new or need a refresher on certain topics, they are covered in my previous articles. Here are some links:

* [Installing framer motion](https://blog.noelcserepy.com/how-to-animate-svg-paths-with-framer-motion#heading-installing-framer-motion)
    
* [Variants](https://blog.noelcserepy.com/how-to-animate-svg-paths-with-framer-motion#heading-variants)
    
* [Keyframes](https://blog.noelcserepy.com/creating-keyframe-animations-with-framer-motion)
    
* [useAnimate](https://blog.noelcserepy.com/maximizing-the-power-of-framer-motion-with-useanimationcontrols)
    

There is a CodeSandbox at the start and end of the article, so don't worry about copying everything down as you go. Let's get into it!

## The Plan

There are 3 parts to this animation:

1. **The cursor** - The blinky line at the end of the text to show where the cursor is.
    
2. **The text** - "Dear Hiring Manager" and the awkward sentences that appear letter by letter as if someone was typing.
    
3. **The layout** - A simplified Google doc to act as a container.
    

We'll create separate react components for each to keep things organised.

## The Cursor

In essence, it's just a line that comes and goes. We can create a `motion.div` with a width of `1px` and add a variant called `blinking`.

```typescript
import { motion } from "framer-motion";

const cursorVariants = {
  blinking: {
    opacity: [0, 0, 1, 1],
    transition: {
      duration: 1,
      repeat: Infinity,
      repeatDelay: 0,
      ease: "linear",
      times: [0, 0.5, 0.5, 1]
    }
  }
};

export default function CursorBlinker() {
  return (
    <motion.div
      variants={cursorVariants}
      animate="blinking"
      className="inline-block h-5 w-[1px] translate-y-1 bg-slate-900"
    />
  );
}
```

The variant uses keyframes to animate the opacity from `0` to `1`. We use four keyframes and define `times` in the transition. This makes the cursor opacity `0` for half the duration and `1` for the other half. Notice how the `times` for keyframes two and three are 0.5. That means that the opacity will switch **instantly** from `0` to `1` instead of fading in and out. This repeats infinitely.

By making the cursor `inline-block` instead of `block`, it will appear next to our text instead of below. That way, when we add more letters to our text, the cursor will be **pushed** to the right.

## Simple Text Animation with Motion Values

There are two parts to this. I want "Dear Hiring Manager" to animate only **once** when the component mounts. **After that**, the awkward sentences should animate in on a new line. We will cover that in the next section.

First, let's create the "Dear Hiring Manager" animation. The idea is this:

1. Take some text.
    
2. Create a count variable.
    
3. Animate the count between `0` and `text.length`.
    
4. Display a substring of text where its length is based on count.
    

### Component Setup

```typescript
import { motion, useMotionValue, useTransform, animate } from "framer-motion";
import CursorBlinker from "./CursorBlinker";

export default function TextAnim() {
  const baseText = "Dear Hiring Manager, ";
  const count = useMotionValue(0);

  return (
    <span className="">
      <motion.span>
	      // Our text goes here 
      </motion.span>
      <CursorBlinker /> // As text grows, the cursor will be "pushed" along
    </span>
  );
}
```

Adding our text into a `<span>` element will allow our cursor to sit next to it because they are both `inline`.

To fully use framer motion, we want to use `useMotionValue()` and not `useState()`. Motion values use Refs under the hood and don't re-render the entire component every time they change. Motion values can run faster and give us more control by exposing easing functions, delays, loops, etc.

### Animation

Now, we can add the animation. Because we want to animate `count` only once when the component mounts, we can use `useEffect()` with an empty dependency array.

```typescript
  const baseText = "Dear Hiring Manager, " as string;
  const count = useMotionValue(0);

  useEffect(() => {
    const controls = animate(count, baseText.length, {
      type: "tween", // Not really needed because adding a duration will force "tween"
      duration: 1,
      ease: "easeInOut",
    });
    return controls.stop;
  }, []);
```

Inside `useEffect()`, we call framer motion's `animate()` function that allows us to imperatively animate a single value. It works like this:

`animate(variableToAnimate, variableTargetValue, transitionOptions)`

We want to animate `count` from `0` (set at initialisation) to `baseText.length`. I also added a transition to control how the text will appear. I'm going with a simple "tween" animation and an "easeInOut" easing function for our purposes.

> "If you want to get fancy, you could adjust the duration of the animation based on the length of the text, but I'll keep it simple and use one second as a starting point."

`return controls.stop` cleans up the animation when the component is unmounted. Don't worry about that too much.

Great, now when our component mounts, `count` is animated from `0` to `baseText.length`.

### Connecting the dots

It's time to connect the `baseText` to `count`. For that, we need `useTransform()`.

`useTransform()` can take a motion value as an argument and returns a new, changed motion value. If you haven't used it before, think of `useTransform()` as "computed values" or linking two values together. When one changes, the other changes, too. There are two ways we can control how the new motion value changes:

1. By adding an input range and an output range:
    

```typescript
const newValue = useTransform(count, [0, 1], [0, 100])
// When `count` goes from `0` to `1`, I want `newValue` to go from `0` to `100`.
```

1. By defining a function that runs every time the input changes:
    

```typescript
const newValue = useTransform(count, (latest) => {latest * 100})
// When `count` changes, I want `newValue` to be 100 times that of `count`.
```

We will use the second method like so:

```typescript
  const rounded = useTransform(count, (latest) => Math.round(latest));
  const displayText = useTransform(rounded, (latest) =>
    baseText.slice(0, latest)
  );
```

We create the `rounded` motion value, simply an integer version of our `count`. We do this because we can only slice a string by discrete values. Typing half a letter also doesn't make much sense 🤔.

The second motion value we create is the text we will display on the page. It uses the `rounded` variable to slice our `baseText` into a substring. This part links our animated `count` variable to the text we want to show.

And that's it! We've animated some text! Here's everything together:

```typescript
import { motion, useMotionValue, useTransform, animate } from "framer-motion";
import { useEffect, useState } from "react";
import CursorBlinker from "./CursorBlinker";


export default function TextAnim() {
  const baseText = "Dear Hiring Manager, " as string;
  const count = useMotionValue(0);
  const rounded = useTransform(count, (latest) => Math.round(latest));
  const displayText = useTransform(rounded, (latest) =>
    baseText.slice(0, latest)
  );

  useEffect(() => {
    const controls = animate(count, baseText.length, {
      type: "tween",
      duration: 1,
      ease: "easeInOut",
    });
    return controls.stop;
  }, []);

  return (
    <span className="">
      <motion.span>{displayText}</motion.span>
      <CursorBlinker />
    </span>
  );
}
```

## Infinite Typing Animation

Okay, it's time for the cringe sentences. We know how to reveal the text, but what if we want to delete the text? And what about typing something different every time?

Here's the gist of it:

1. We reverse the animation so that `count` goes from `0` to `1` and then back to `0`.
    
2. We repeat this indefinitely.
    
3. We change the text every time the count hits `0`.
    

You're all grown up now, armed to the teeth with animation knowledge, so here's the entire code for this component. We'll dissect them together below.

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

export default function RedoAnimText() {
  const textIndex = useMotionValue(0);
  const texts = [
    "I am writing to you because I want a job.",
    "I am the best candidate for this job.",
    "In my grand adventure as a seasoned designer...",
    "Knock knock! Who's there? Your new employee!",
    "Walking the tightrope balance of project management...",
    "I find myself compelled to express my interest due to...",
    "My pen (or should I say, keyboard) is at work today because...",
    "Inspired by the alluring challenge in the job posting, I am writing...",
    "Stirred to my keyboard by the tantalising nature of the role…"
  ];

  const baseText = useTransform(textIndex, (latest) => texts[latest] || "");
  const count = useMotionValue(0);
  const rounded = useTransform(count, (latest) => Math.round(latest));
  const displayText = useTransform(rounded, (latest) =>
    baseText.get().slice(0, latest)
  );
  const updatedThisRound = useMotionValue(true);

  useEffect(() => {
    animate(count, 60, {
      type: "tween",
      duration: 1,
      ease: "easeIn",
      repeat: Infinity,
      repeatType: "reverse",
      repeatDelay: 1,
      onUpdate(latest) {
        if (updatedThisRound.get() === true && latest > 0) {
          updatedThisRound.set(false);
        } else if (updatedThisRound.get() === false && latest === 0) {
          if (textIndex.get() === texts.length - 1) {
            textIndex.set(0);
          } else {
            textIndex.set(textIndex.get() + 1);
          }
          updatedThisRound.set(true);
        }
      }
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <motion.span className="inline">{displayText}</motion.span>;
}
```

### Variables

First of all, we have some new variables:

* `texts` holds all our texts.
    
* `textIndex` keeps track of which text we're currently on.
    
* `baseText` is now a motion value derived from the `text` and `textIndex`.
    
* `updatedThisRound` helps us change the text only once per loop.
    

### Animation

The animation is almost the same as the first, but with a few key differences:

* I use `60` as a hardcoded length because a dynamic value won't change between repeats.
    
* I added `repeat: Infinity` for an endless loop.
    
* `repeatType: "reverse"` makes the animation go backwards instead of starting from the beginning. This simulates the person hitting "backspace".
    
* `repeatDelay: 1` gives the user a second to read the complete sentence before being deleted.
    

### Swapping the Text

The tricky part is swapping the text when `count` is `0`. Transition objects have some handy triggers that allow us to call a function during certain stages of our animation. We can use those to increment `textIndex` to swap out our current text.

`onComplete()` gets triggered at the end of our animation, but since our animation never ends, it never gets called.

We need a trigger that gets called every repeat. There is no `onRepeat()` in framer motion, so we must build it ourselves.

`onUpdate()` lets us call a function on every frame. But simply checking if `latest === 0` and incrementing `textIndex` won't work because `onUpdate()` is called sixty times per second (or whatever your browser's frame rate is). Since we're waiting at `count = 0` for 1 second, we would increment `textIndex` sixty times!

That's where `updatedThisRound` comes in. It acts as a latch, allowing us to increment `textIndex` only once per repeat. It has to be a motion value because we don't want our component to re-render when its value changes. Here's how it works:

```typescript
onUpdate(latest) {
	// If we updated already and we're not at 0 anymore, 
	// set updatedThisRound to false.
	// The next time we hit 0, we will increment.
	if (updatedThisRound.get() === true && latest > 0) {
	  updatedThisRound.set(false);
	  
	// If we haven't updated yet and we're at 0,
	// increment and set updatedThisRound to true.
	} else if (updatedThisRound.get() === false && latest === 0) {
	
	// Set textIndex to 0 if we reach the end of our texts array.
	// So we don't run out of silly sentences
	  if (textIndex.get() === texts.length - 1) {
		textIndex.set(0);
	  } else {
		textIndex.set(textIndex.get() + 1);
	  }
	  updatedThisRound.set(true);
	}
  }
```

And yes, running a bunch of conditionals 60 times per second sends me straight to performance hell, but hey... I want silly sentences. And besides, there's only a little else on my homepage, so I figured it was okay. Let me know if you have a better solution :)

## The Layout

Here, we keep it simple. We need a container that looks like a Google Doc. I decided to take a couple of elements like the logo, the document title ("untitled document", of course) and the formatting toolbar buttons.

I used an aspect ratio of 1/1.41, the same as A4. The rest is just CSS.

I added a short enter animation to the layout. The child components come in slightly later with `delayChildren` and `staggerChildren`.

```typescript
"use client";
import { DocumentTextIcon } from "@heroicons/react/20/solid";
import { motion } from "framer-motion";
import AnimText from "./AnimText";

const containerVariants = {
  hidden: {
    opacity: 0,
    y: 30
  },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.3,
      ease: "easeOut",
      delayChildren: 0.3,
      staggerChildren: 0.1
    }
  }
};

const itemVariants = {
  hidden: {
    opacity: 0,
    y: 15
  },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.3,
      ease: "easeOut"
    }
  }
};

export default function A4Animation() {
  return (
    <motion.div className="flex w-full select-none items-center justify-center ">
      <motion.div
        variants={containerVariants}
        animate="visible"
        initial="hidden"
        className="flex aspect-[1/1.41] h-[500px] flex-col rounded-2xl bg-white p-2"
      >
        <motion.div
          variants={itemVariants}
          className="flex items-center space-x-2"
        >
          <DocumentTextIcon className="h-8 w-8 text-indigo-700" />
          <span className="text-slate-700">Untitled document</span>
        </motion.div>
        <motion.div
          variants={itemVariants}
          className="flex items-center justify-center"
        >
          <div className="mt-2 flex space-x-4 rounded-full bg-slate-100 px-8 text-slate-700">
            <strong>B</strong>
            <span className="font-italic">I</span>
            <span className="underline">U</span>
            <strong className="underline">A</strong>
          </div>
        </motion.div>
        <motion.span
          variants={itemVariants}
          className="inline h-full w-full p-8 text-lg text-slate-900"
        >
          <AnimText />
        </motion.span>
      </motion.div>
    </motion.div>
  );
}
```

## Finishing Touches

Below is the CodeSandbox of the entire effect. I added some line breaks in the final version when the first animation ends. There is also a delay to make sure one happens after the other. Check it out on your own time to see how it works. Leave me a comment if you need any help.

<iframe src="https://codesandbox.io/embed/text-typing-animation-rtzvgl?fontsize=14&hidenavigation=1&module=%2Fsrc%2Fcomponents%2FA4Layout.tsx&theme=dark&view=editor" style="width:100%;height:500px;border:0;border-radius:4px;overflow:hidden" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>

## Conclusion

Animation complete! Congratulations! 🍾

Thank you for reading. I hope you learned something. As you know, I just launched [CoverCraft](https://www.covercraft.io/), and I would be exceedingly happy if you tried it out. It's still early days, but any feedback is welcome. I am thinking about writing a series on how I built CoverCraft. Let me know if you would be interested in something like that.

Have a great day :)
