3 Simple Text Animations That You Can Build Using Framer Motion
Samit Kapoor

Samit Kapoor @samitkapoor

About: SDE | Creator of stackbits.dev & readmechef.com | Into coding, cars, tennis, photography.

Location:
Delhi, India
Joined:
Oct 13, 2022

3 Simple Text Animations That You Can Build Using Framer Motion

Publish Date: Jun 28
1 0

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

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;
Enter fullscreen mode Exit fullscreen mode

What's going on here?

  • We're importing motion from framer-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%)`);
  }
...
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

and Voila! there's your first text animation.

Wavy Text

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.

Failed 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>;
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!

Comments 0 total

    Add comment