Let's face it: animations make websites more interesting. They're great for grabbing attention and guiding people around the page. Almost all the AI SaaS websites landing pages will have them.
The best part is that you don't have to be a wizard to implement them. In this article, I'm going to show you 3 simple text animations that you can build using framer motion.
1. Rainbow Text
Think of it like this: instead of a single, solid color, your text has a gradient. And this gradient isn't static, it's constantly changing! The color at the end of the current gradient seamlessly flows into the beginning of the next, creating a continuous, mesmerizing rainbow effect.
Let's build this step by step:
Set up your component: We'll start by creating a React component and adding some text. Instead of a fixed color, we'll give it an initial gradient.
import React, { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
type RainbowTextProps = {
className?: string;
children?: ReactNode;
};
const RainbowText = ({ className = '', children }: RainbowTextProps) => {
return (
<motion.span
initial={{
backgroundImage: 'linear-gradient(90deg, #FF4D4D 0%, #FF944D 100%)'
}}
className={cn('text-transparent bg-clip-text', className)}
>
{children}
</motion.span>
);
};
export default RainbowText;
What's going on here?
- We're importing
motion
fromframer-motion
. This is what allows us to animate the text. - We're using
initial
to set the starting gradient for the text. -
text-transparent bg-clip-text
are tailwindcss classes that make the text transparent and clip the background to the text (so the gradient shows through).
Important Note: If you have trouble understanding the above code, I'd recommend pausing here and checking out the basics of the Framer Motion.
Create an array of gradients: We need to make the gradient colors change over time, creating that flowing rainbow effect. To do this, we'll introduce a colors prop and create an array of linear gradients. We make sure the color at the end of the current gradient seamlessly flows into the beginning of the next.
...
const RainbowText = ({
className = '',
children,
colors = [
'#FF4D4D',
'#FF944D',
'#FFC14D',
'#E8FF4D',
'#6DFF4D',
'#4DFFA1',
'#4DFFFF',
'#4DAAFF',
],
duration = 2
}: RainbowTextProps) => {
const linearGradients = [];
for (let i = 0; i < colors.length - 1; i++) {
linearGradients.push(`linear-gradient(90deg, ${colors[i]} 0%, ${colors[i + 1]} 100%)`);
}
...
Bring it all together: Now just pass on this linearGradients
array to backgroundImage
style in animate
prop of the span to complete the animation.
Full implementation:
import React, { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
type RainbowTextProps = {
className?: string;
children?: ReactNode;
colors?: string[];
duration?: number;
};
const RainbowText = ({
className = '',
children,
colors = [
'#FF4D4D',
'#FF944D',
'#FFC14D',
'#E8FF4D',
'#6DFF4D',
'#4DFFA1',
'#4DFFFF',
'#4DAAFF',
'#4D6DFF',
'#6D4DFF',
'#A14DFF',
'#D14DFF',
'#FF4DAA',
'#FF4D6D',
'#FF4D4D',
'#FF944D'
],
duration = 2
}: RainbowTextProps) => {
const linearGradients = [];
for (let i = 0; i < colors.length - 1; i++) {
linearGradients.push(`linear-gradient(90deg, ${colors[i]} 0%, ${colors[i + 1]} 100%)`);
}
return (
<motion.span
initial={{ backgroundImage: linearGradients[0] }}
animate={{ backgroundImage: linearGradients }}
transition={{
duration: duration,
ease: 'linear',
repeat: Infinity
}}
className={cn('text-transparent bg-clip-text', className)}
>
{children}
</motion.span>
);
};
export default RainbowText;
and Voila! there's your first text animation.
Wavy Text
Think of it like this: Every character in the text is animating on its own, and each character bounces up and down but they don't all start at the same time, there's a delay to when they start bouncing which creates a wave.
Let's build this step by step:
Setup your component: Start by initializing the component, defining the necessary props, and rendering the text.
type WavyTextProps = {
text: string;
className?: string;
};
const WavyText = ({ text, className = '' }: WavyTextProps) => {
return <div className={`flex flex-wrap overflow-visible p-4 ${className}`}>{text}</div>;
};
Make each character an individual element: Since I told you each character is animating on its own, we have to break the text and render each character separately like so
<div className={`flex flex-wrap overflow-visible p-4 ${className}`}>
{text.split('').map((char, index) => {
return <motion.span key={index}>{char === ' ' ? '\u00A0' : char}</motion.span>;
})}
</div>
Add bouncing properties to each character and bring it all together
Full implementation:
import { motion } from 'framer-motion';
type WavyTextProps = {
text: string;
className?: string;
};
const WavyText = ({ text, className = '' }: WavyTextProps) => {
return (
<div className={`flex flex-wrap overflow-visible p-4 ${className}`}>
{text.split('').map((char, index) => {
return (
<motion.span
key={index}
initial={{ y: -15 }}
animate={{ y: [0, -15] }}
transition={{
type: 'spring',
stiffness: 100,
damping: 20,
delay: index * 0.1,
repeat: Infinity,
repeatType: 'reverse'
}}
>
{char === ' ' ? '\u00A0' : char}
</motion.span>
);
})}
</div>
);
};
export default WavyText;
What's going on here:
- We set the initial
y
value to -15, so each character starts 15 pixels above its resting position. - The
animate
prop moves each character between 0 and -15 pixels, creating the bouncing motion. - The spring transition adds realistic physics, making each bounce feel natural.
- The delay, multiplied by the character’s index, staggers the animation and creates the wave effect.
Blur Text
Think of it like this: Each character starts out slightly blurred and shifted downward from its final position. As the animation plays, each character sharpens into focus and smoothly rises to its resting place.
Let's build this step by step:
So, the first two steps are actually the same as the Wavy Text, let's build from there:
import { motion } from 'framer-motion';
type BlurTextProps = {
text: string;
className?: string;
};
const BlurText = ({ text, className }: BlurTextProps) => {
return (
<div className={`flex flex-wrap overflow-visible ${className}`}>
{text.split('').map((char, index) => (
<motion.span key={index} className="inline-block">
{char === ' ' ? '\u00A0' : char}
</motion.span>
))}
</div>
);
};
export default BlurText;
Now, let’s add the animation: each character will start blurred and slightly lower than its final position, then animate into focus and rise up to its resting place. We’ll also stagger the animation for each character to create a smooth, sequential effect.
Full implementation:
import { motion } from 'framer-motion';
type BlurTextProps = {
text: string;
className?: string;
};
const BlurText = ({ text, className = '' }: BlurTextProps) => {
return (
<div className={`flex flex-wrap overflow-visible ${className}`}>
{text.split('').map((char, index) => (
<motion.span
key={index}
initial={{ y: 10, filter: 'blur(10px)' }}
whileInView={{ y: 0, filter: 'blur(0px)' }}
transition={{
duration: 0.2,
delay: index * 0.1,
ease: 'backOut'
}}
className="inline-block"
>
{char === ' ' ? '\u00A0' : char}
</motion.span>
))}
</div>
);
};
export default BlurText;
What's going on here:
- Each character starts with a
y
value of 10 (10 pixels below its final position) and a heavy blur applied. - The
whileInView
prop animates each character upward to its resting position and removes the blur, making the text crisp and clear. - The transition uses a small duration and a staggered delay based on the character’s index, so each letter animates one after another, creating a smooth reveal effect.
Why do we use the whileInView
prop here, but the animate
prop in Wavy Text?
The animate
prop triggers the animation immediately when the component renders, regardless of whether the text is visible on the screen. In contrast, the whileInView
prop starts the animation only when the text actually comes into the user's viewport. For the blur text effect, using animate
could cause the animation to play off-screen, so users might miss it entirely. By using whileInView
, we ensure the animation runs at the right moment when the user can actually see it.
If you enjoyed these animations and want to discover even more, check out my open source animated library, stackbits.dev, for a collection of ready-to-use animation components!
Hi, I’m Samit, a software engineer and freelancer passionate about building real world projects. If you’re looking to collaborate or just want to say hi, check out my portfolio. Let’s connect!