Animated Text Tutorial | React & Motion
React & Motion | Tutorial

The Goal
Every now and then I have the urge to make something fun. I enjoy a smooth user-experience and animation libraries are making it easier every year to implement fun experiences that are supported by almost all modern browsers.
For simonbremner.net, I decided to add Motion as my animation library. It's not the most lightweight or the most customizable, but the setup and the development cost is minimal as long as you're happy with their pre-built animations.
For this example, I want to create the effect seen here: Split text | Motion examples with a couple of modifications. I want the text to animate in based on my scroll position and I want it to animate out when I scroll back up the page.
Additionally, I prefer TypeScript over Vanilla JS, so everything in this post will be in TSX format. If you'd like to use JSX or even Vanilla JS without react, there are tutorials on: Docs - Motion. They're basically the same...ish.

The Setup
I'm not going to go through the process of setting up a React Application. If you're reading this, you should already have created a React Application or have React imported into your project. Obviously, meta frameworks like Next and Remix are also viable. For this example, I'm using Astro as my framework because I think it's extra shiny right now, but you won't see any Astro-specific code here.
Anyways, depending on your favourite package manager, here are your installation commands:
// pnpm
pnpm install motion
// npm
npm install motion
// yarn
yarn add motion
Then, import the necessary packages into your component. For this project, we are only going to be using motion
, useScroll
, and useTransform
from Motion's library:
import { motion, useScroll, useTransform } from "motion/react"
import { useRef } from "react"
The Main Component
This is where we'll define the main AnimatedText.tsx
component. It's pretty straightforward - it receives the content: string
prop and passes it, along with some animation details, to the AnimatedWord.tsx
component that we will create next.
I forgot to mention that I'm using TailwindCSS here. That's why I'm using flex
& flex-wrap
as my className
const AnimatedText = ({ text }: {
text: string,
}) => {
// Create sectionRef for scroll tracking
const sectionRef = useRef<HTMLDivElement>(null);
// Track scroll progress of sectionRef in the browser's viewport
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "center center"],
});
// Split text into individual words
const words = text.split(' ').filter(word => word.length > 0);
return (
<motion.div ref={sectionRef}>
<div className="flex flex-wrap">
{words.map((word, wordIndex) => (
<AnimatedWord
key={`word-${wordIndex}`}
word={word}
wordIndex={wordIndex}
totalWords={words.length}
scrollYProgress={scrollYProgress}
/>
))}
</div>
</motion.div>
);
};
The details:
sectionRef
defines the element we want to target in the DOMuseScroll
tracks the position of the target element within the browser's viewporttarget: sectionRef
tells motion what element to trackoffset: ["start end", "center center"]
goes like this:start
is the vertical location oftarget
end
is the vertical location of the viewport- The two arguments in the array define where the animation will
start
andend
respectively
words
splits each word in the string and passes them into theWordAnimation.tsx
component through amap
function
Animating Individual Words
Now that we are passing each word to this component as well as the scroll position of the section as a whole, we can do some fun things like stagger each word to animate based on its position in the array of words.
const WordAnimation = ({
word,
wordIndex,
totalWords,
scrollYProgress,
}: {
word: string,
wordIndex: number,
totalWords: number,
scrollYProgress: MotionValue<number>,
}) => {
// Calculations to set when this word should appear
const wordStart = (wordIndex / totalWords) * 0.75;
const wordEnd = wordStart + 0.05;
// Calculate the opacity and y position of each word
const opacity = useTransform(
scrollYProgress,
[wordStart, wordEnd],
[0, 1]
);
const y = useTransform(
scrollYProgress,
[wordStart, wordEnd],
[20, 0]
);
return (
<motion.div
style={{
opacity,
y,
marginRight: "0.4em",
marginBottom: "0.1em",
display: "inline-block",
}}
className="text-foreground font-normal"
>
{word}
</motion.div>
);
};
The details:
wordStart
andwordEnd
are pretty self-explanatory. I'm just calculating when to start and end each animation. Play with these numbers to see what suits you- The third argument in
useTransform
is providing the expected values at each calculated point ([0, 1] just meansopacity: 0
andopacity: 1
when passed intostyle
ofmotion.div
Using the Component
To use our component, all we have to do is pass a string of words into it. It may look pretty basic right now, but you can pass your own text styles, spacing, etc. to make it your own!
<AnimatedText
text="Peter Piper doesn't like peppers."
/>
Wrapping Up
And that's it! A scrolling text animation component that's lightweight, customizable, and adds some serious polish to your site.
Here are some extra things to try:
- Section Size: Since we're animating based on the position of the section in the viewport, be careful with long content. For larger sections of text, it would be worth mapping each paragraph in separate components.
- Highlighting: Highlighting individual words is useful for drawing attention to important content. You could pass an array of word indexes to highlight individual words. Then create a ternary operator that applies styles to each word if it's in the array.
- Customization: Play with the timing, positioning, or add new animations. I used opacity and y-position, but you could have each word rotate slightly, scale up/down, or even have them come from off the screen. Enjoy the creative process!
The most important thing is to have fun with it!