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:
- The basics of animating components with Framer Motion
- Animating child components in relation to their containers
- 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> COPYSince 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> COPYOpen 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> COPYPerfect! 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.
<Framesize={ "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> COPYThe 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> COPYLooking 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 }/> COPYThat 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" }/> COPYNow 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> COPYBecause 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 containerconst containerVariants = { before: {}, after: { transition: { staggerChildren: 0.3 } },} COPYAnd 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 textconst textVariants = { before: { y: -26 * 1.2, opacity: 0.6, }, after: { y: 0, opacity: 1, transition: { ease: "easeOut", duration: 0.4 }, },} COPYGreat, 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 containerconst lineContainerVariants = { before: {}, after: { transition: { delayChildren: 0.3 * 3 } },} COPYFinally, 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 linesconst lineVariants = { before: { opacity: 0, width: 0, }, after: { opacity: 1, width: 20, transition: { ease: "easeIn", duration: 0.2 }, },} COPYAnd 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 lettersconst string = Array.from("First Line") COPYNow 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> COPYIt 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> )} COPYAll 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 containerconst containerVariants = { before: {}, after: { transition: { staggerChildren: 0.06 } },} // Variants for animating each letterconst 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, }, },} COPYAdvanced 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.




