Animated Text with Framer Motion

30m

Intermediate

I believe in the power of motion. As designers, we’ve seen firsthand how interactivity can take our work to the next level. But as users ourselves, we understand that fluid animations are now an expected part of our digital experience. And now, with Framer Motion, an open source React library that powers production-ready animations, it’s easier than ever to create moving components—even with very little code experience.

In this guide, you’ll learn the three fundamental concepts of animating text:

  1. The basics of animating components with Framer Motion
  2. Animating child components in relation to their containers
  3. Animating individual letters

Each topic comes with a starter and final file, so you can follow along as you go.

Download the Starter and Final Files ›

Basics of Animation

In order to grasp the basics of animating text, let’s build out a simple component. We’ll begin by animating two lines that are revealed from the middle, as demonstrated below.

Open the starter file, and navigate to the code panel. Inside the Middle Reveal.tsx file, you’ll see that a few frames have already been set up for you. Notice how each text frame is wrapped within a clipping frame, then positioned within the container.

Before we begin, take a quick look at the code for the top mask frame. You’ll see that it has been set to a height slightly larger than the font size 26, and that the y position has been moved up by the same amount. This is crucial to this animation as it positions the two text lines on top of one another, instead of overlapping each other.

<Frame
width={ "100%" }
height={ 26 * 1.2 }
y={ -26 * 1.2 }
background={ "" }
>
(...)
</Frame>
COPY

Since we’re here to animate, let’s get straight into it! In Framer Motion, you can add two attributes to a frame: initial and animate. initial will tell the component where to start out, and animate will tell the component how to animate it.

We want the top line to slide up from below. Right now it is positioned in the center, so we’re going move down the initial y position. This can be any number, but since we only want it to go slightly outside the masking frame, I’m going to set it to the size of the font, multiplied by 1.2: initial={{ y: 26 * 1.2 }}. And because we want the text to slide into place when the frame loads, we’re going to set the animate value to 0: animate={{ y: 0 }}. Note that all positions will be relative to the frame surrounding it.

<Frame
size={ "100%" }
background={ "" }
style={{
fontFamily: "Montserrat, Work Sans, sans-serif",
fontWeight: "bold",
letterSpacing: "-0.04em",
fontSize: 26,
color: "#FFF",
}}
initial={{ y: 26 * 1.2 }}
animate={{ y: 0 }}
>
First Line
</Frame>
COPY

Open the preview window and check it out. Your text should be moving as desired! However note that the text looks like it’s jumping up instead of sliding in. In order to control this movement, let’s add a transition prop to our frame. Inside this prop, we can define the type of animation, such as tween, spring, or inertia, set the duration of the animations, and delay them as well. Right now we want the text to ease in and probably speed up the animation as well, so we’re going to add the following to the frame: transition={{ ease: "easeOut", duration: 0.4 }}.

<Frame
size={ "100%" }
background={ "" }
style={{
fontFamily: "Montserrat, Work Sans, sans-serif",
fontWeight: "bold",
letterSpacing: "-0.04em",
fontSize: 26,
color: "#FFF",
}}
initial={{ y: 26 * 1.2 }}
animate={{ y: 0 }}
transition={{ ease: "easeOut", duration: 0.4 }}
>
First Line
</Frame>
COPY

Perfect! Let’s do the same for the bottom line too. This time, we’re going to make it fall from the top, so we’ll need to set the initial y position to a negative number.

<Frame
size={ "100%" }
background={ "" }
style={{
fontFamily: "Montserrat, Work Sans, sans-serif",
fontWeight: "bold",
letterSpacing: "-0.04em",
fontSize: 26,
color: "#FFF",
}}
initial={{ y: -26 * 1.2 }}
animate={{ y: 0 }}
transition={{
ease: "easeOut",
duration: 0.4,
}}
>
Second Line
</Frame>
COPY

The animation looks pretty good as it is. However, in the original example you’ll see that the text is being clipped. This is where the mask frames kick in, except they are not masking the text yet. This is a simple matter of adding overflow={ "hidden" } to both the top and bottom masking frames.

<Frame
width={ "100%" }
height={ 26 * 1.2 }
y={ -26 * 1.2 }
overflow={ "hidden" }
background={ "" }
>
(...)
</Frame>
COPY

Looking good! Everything should be in place now. Let’s move on to something fancier!

Work with Children

We’ve learned how to animate a single component, but what happens if we want to animate things in relation to each other? This is where concepts like variants, staggerChildren, and delayChildren come in. Framer Motion provides a quick and easy way for the sub-elements of a container (or children) to communicate with its parent (the wrapping container).

This time, we’re going to create the same clipping effect, except we’re going to make the three lines of text stagger and also add a fancy line animation at the end.

The text and the lines have been positioned and styled for you in the starter file. You’ll see that instead of copy and pasting the text and its mask container three times, I am using .map() to loop through each line of text and create the same component. More information about this functionality can be found here.

The component is composed of two big containers; the container for the text and the container for the frame. We’re going to add the staggering effect to the text container, then delay the line animation until the text has finished animating.

Remember when we added animations directly to the components?

<Frame
initial={ Initial State }
animate={ Animated State }
transition={ Transition Details }
/>
COPY

That same functionality can be achieved by using a variant!

const frameVariants = {
before: { Initial State },
after: { Animated State, transition: { Transition Details }}
}
<Frame
variants={ frameVariants }
initial={ "before" }
animate={ "after" }
/>
COPY

Now you may be wondering why you would use this method when it is more efficient to simply type in the animations. Well, by using variant, you can actually reuse the same animations across multiple components, saving you from repeating your work in the future.

An even better reason is that if you have a parent and child element that share the same variant names (e.g. before, after), they are automatically linked. This makes it possible to add effects such as staggering and delaying. Alright, let’s see this in action.

At the top, a few variables have already been created for you. Let’s go ahead and assign these to their corresponding frames.

<Frame
(...)
>
<Frame
height={ 26 * 1.2 * 3 + 6 }
width={ "100%" }
center={ "y" }
background={ "" }
variants={ containerVariants } // Add variants to container
initial={ "before" } // Add initial state
animate={ "after" } // Add animated state
>
{items.map((item, i) => (
<Frame
(...)
>
<Frame
size={ "100%" }
background={ "" }
style={{
fontFamily: "Montserrat, Work Sans, sans-serif",
fontWeight: "bold",
letterSpacing: "-0.04em",
fontSize: 26,
color: "#FFF",
}}
variants={ textVariants } // Add variants
>
{item}
</Frame>
</Frame>
))}
<Frame
size={ "100%" }
background={ "" }
variants={ lineContainerVariants } // Add variants
>
<Frame
background={ "" }
height={ 2 }
y={ 26 * 1.2 * 3 + 8 }
backgroundColor={ "#FFF" }
left={ "50%" }
variants={ lineVariants } // Add variants
/>
<Frame
background={ "" }
height={ 2 }
y={ 26 * 1.2 * 3 + 8 }
backgroundColor={ "#FFF" }
right={ "50%" }
variants={ lineVariants } // Add variants
/>
</Frame>
</Frame>
</Frame>
COPY

Because the variant names for the parent and children elements (before and after in this case) are identical, we can now assign a stagger transition to the text container and all of the text frames will be affected.

Notice how initial and animate are only assigned to the parent frame. In the nature of React, all of the properties of the parent elements will be passed down to its children. Assigning these two values to each of the children will override the parent values, causing the link to break.

All we have to do now is add the animations. Let’s add a stagger of 0.3 seconds to the container.

// Add staggering effect to the children of the container
const containerVariants = {
before: {},
after: { transition: { staggerChildren: 0.3 } },
}
COPY

And just like the previous example, we’re going to make the text fall into each mask by animating the y position. Let’s also make the opacity change from 0.6 to 1 to add an extra touch.

// Variants for animating the text
const textVariants = {
before: {
y: -26 * 1.2,
opacity: 0.6,
},
after: {
y: 0,
opacity: 1,
transition: { ease: "easeOut", duration: 0.4 },
},
}
COPY

Great, all your text animations are now working! All we have to do is animate the lines. We want them to begin after all of the text has appeared on the screen, so we’re going to add a delay to the container. Now the lines will begin animating 0.3 * 3 seconds after the frame has been loaded.

// Variants for animating the line container
const lineContainerVariants = {
before: {},
after: { transition: { delayChildren: 0.3 * 3 } },
}
COPY

Finally, let’s animate the line. The line you see at the bottom is actually two lines, each positioned 50% to the right and left. Because Framer Motion lets you animate most CSS values, all we have to do is make the width change from 0 to something else.

// Variants for animating the lines
const lineVariants = {
before: {
opacity: 0,
width: 0,
},
after: {
opacity: 1,
width: 20,
transition: { ease: "easeIn", duration: 0.2 },
},
}
COPY

And there you have it! Pop open the preview window and see your animations in action! For our final act, let’s learn how to animate each letter individually.

Animate Individual Letters

Making individual letters animate is pretty simple, especially if we apply everything that we’ve just learned. For the animation above, you’ll need to put each of the letters in a frame (as opposed to the whole line) and animate each frame individually. Then you’ll probably want to wrap the whole line in a text container, and apply a stagger on it. Pretty simple, right?

Theoretically, yes. However, as soon as you dive in you’ll run into a problem: how do you create a separate frame for each of the letters without writing a hundred lines of code? Luckily, this problem can easily be solved by arrays.

Take the string that you are trying to animate, and create an array of letters by using Array.from(). This will return the following: ["F", "i", "r", "s", "t", " ", "L", "i", "n", "e"].

// Create an array of letters
const string = Array.from("First Line")
COPY

Now all we have to do is map through this array of letters and create a frame for each. The basic syntax has already been laid out for you, but if you open the preview, you’ll see that the frames are not properly positioned. This is because the frames default to display: block and have a default width and height. Let’s add some CSS values to make it look nicer.

<Frame
center={ "y" }
height={ 26 }
width={ "100%" }
background={ "" }
style={{
fontFamily: "Montserrat, Work Sans, sans-serif",
fontWeight: "bold",
letterSpacing: "-0.04em",
fontSize: 26,
color: "#FFF",
display: "flex", // Set the display value to flex
justifyContent: "center", // Center all children elements on the x axis
}}
variants={ containerVariants }
initial={ "before" }
animate={ "after" }
>
{string.map((letter, index) => (
<Frame
key={ index }
width={ "auto" } // Set the width to the width of the letter
height={ 26 } // Set the height to the height of the text
background={ "" }
style={{ position: "relative" }} // Position elements
variants={ letterVariants }
>
{letter}
</Frame>
))}
</Frame>
COPY

It seems like all of the letters are in-line now, but where did the space go? Unfortunately, we set the width to auto so that the frames would autosize based on the width of the letter. However, because spaces do not have a default size, the width will be set to 0 and therefore disappear.

This can be easily fixed with a non-breaking space, or a space with a fixed width. This character can be referenced with \u00A0. All we have to do is add a conditional statement that states that if the current letter is a space, we want to render a non-breaking space.

{string.map((letter, index) =>
<Frame
key={ index }
width={ "auto" }
height={ 26 }
background={ "" }
style={{ position: "relative" }}
variants={ letterVariants }
>
// Set any spaces to a non-breaking space
{letter === " " ? "\u00A0" : letter}
</Frame>
)
}
COPY

All that’s left is to add the animation values! We’re going to use a spring animation this time, instead of a tween.

// Add staggering effect to the children of the container
const containerVariants = {
before: {},
after: { transition: { staggerChildren: 0.06 } },
}
// Variants for animating each letter
const letterVariants = {
before: {
opacity: 0,
y: 20,
transition: {
type: "spring",
damping: 16,
stiffness: 200,
},
},
after: {
opacity: 1,
y: 0,
transition: {
type: "spring",
damping: 16,
stiffness: 200,
},
},
}
COPY

Advanced Animations

Great work! You can apply everything you’ve learned to create even more complex animations. For example, Framer Motion also lets you animate all sorts of CSS properties, such as rotate and skew. You can get a fancy animation running within seconds just by playing around with these values.

You could also try animating words instead of individual letters.

Or use dynamic variants to animate random orders and effects.

All of these examples and more can be found in the example file below.

Download the Example File ›

You can also download the Animated Text package on the store to use them directly in your designs.

Download the Animated Text Package ›

If you have any questions, find me on Twitter. Make sure to check out our great communities on Spectrum and Facebook as well.