Writing

05/05

4 minutes 805 words

Wavy Text Animation With Framer Motion


Hey y’all, it’s been a while and I’ve been inconsistent with my writing schedule. Now that my AP Computer Science exams are over, and summer is almost here, I will hopefully have more time on my hands to devote to projects and writing.

In our blog post today, we’ll be looking at how we can create a satisfying wavy text animation using Framer Motion, React and TypeScript.

Here’s a demo of the project in CodeSandbox.

Getting Started

I know you’re eager for action, so let’s begin. Start by initialising a React and TypeScript project using create-react-app.

terminal
npx create-react-app wavy-text --template typescript
cd wavy-text

For this, we only need to install one other library called Framer Motion. Let’s install it.

terminal
yarn add framer-motion
# npm i framer-motion

Awesome. Our project is properly setup. Let’s open up our src/App.tsx to get started. Let’s replace the default content to get started.

src/App.tsx
import "./styles.css";
import WavyText from "./WavyText";

export default function App() {
  return (
    <div className="App">
      <h1>Awesome Wavy Text.</h1>
    </div>
  );
}

Cool. Let’s now switch to our src/styles.css file to configure some basic styling for our application. Nothing too fancy, but we want to make it look pretty.

src/styles.css
@import url("https://fonts.googleapis.com/css2?family=Lexend+Deca&display=swap");

body {
  background: linear-gradient(
    45deg,
    hsl(272deg 75% 65%) 0%,
    hsl(193deg 100% 50%) 50%,
    hsl(162deg 84% 88%) 100%
  );
}

.App {
  font-family: "Lexend Deca", sans-serif;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  justify-content: center;
  align-items: center;
}

h1 {
  color: white;
  font-size: 48px;
  user-select: none;
}

Creating The Animation

Awesome. Now that we have that boring stuff setup and working, let’s get into the actual meat of this project.

Switching gears onto React now, let’s create a new file at src/WavyText.tsx and import what we’ll need for this project and configure our props for the component.

src/WavyText.tsx
import { FC } from "react";
import { motion, Variants, HTMLMotionProps } from "framer-motion";

interface Props extends HTMLMotionProps<"div"> {
  text: string;
  delay?: number;
  duration?: number;
}

Since we’re using Motion, we need to use HTMLMotionProps to forward our props onto our HTML component.

Let’s now start to create our React function component inside our file and pass our props through.

src/WavyText.tsx
const Letter: FC<Props> = ({
  text,
  delay = 0,
  duration = 0.05,
  ...props
}: Props) => {

}

Inside here, we should take our text input and transform each letter in this string into an array of strings. For this, we can use the Array.from() function in JavaScript to do achieve exactly what we want.

src/WavyText.tsx
const letters = Array.from(text);

Do note that if you’re using an international language, you might want to check out Grapheme Splitter to divide strings into individual user perceived characters, as opposed to computer perceived characters. Since our text is in English, it’d just add unnecessary complication and an extra step to our project so I’m not adding it in :)

Awesome. Let’s now map individual letters in this array under another component.

src/WavyText.tsx
return (
  <motion.h1
    style={{ display: "flex", overflow: "hidden" }}
    {...props}
  >
    {letters.map((letter, index) => (
      <motion.span key={index}>
        {letter === " " ? "\u00A0" : letter}
      </motion.span>
    ))}
  </motion.h1>
);

Our animation functionality basically works now… there’s just a slight problem. The animation looks terrible. Luckily, we can use Variants in Framer Motion to solve our problem.

Outside (or inside - we can even declare them in a new file and import them in) our WavyText component, we can create two different animations for both the container and the child.

src/WavyText.tsx
const container: Variants = {
  hidden: {
    opacity: 0
  },
  visible: (i: number = 1) => ({
    opacity: 1,
    transition: { staggerChildren: duration, delayChildren: i * delay }
  })
};

const child: Variants = {
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      type: "spring",
      damping: 12,
      stiffness: 200
    }
  },
  hidden: {
    opacity: 0,
    y: 20,
    transition: {
      type: "spring",
      damping: 12,
      stiffness: 200
    }
  }
};

Now that we have that done, we can set the variants in our components to the appropriate animation.

src/WavyText.tsx
<motion.h1
  style={{ display: "flex", overflow: "hidden" }}
  variants={container}
  initial="hidden"
  animate="visible"
  {...props}
>

…and in our child component:

src/WavyText.tsx
<motion.span key={index} variants={child}>

Cheers - our animation now works. We just need to import it into our src/App.tsx file and configure it properly.

Open up the src/App.tsx file now. Start by importing your component, and then delete the <h1></h1> element, and replace it with:

src/WavyText.tsx
// import WavyText from "./WavyText";
// ...

<WavyText text="Awesome Wavy Text." />

Wonderful. Our animation should now be working as we expected. On my example, I’ve also implemented a “replay” functionality, if you’re interested into looking at the code behind that, be sure to check out CodeSandbox

Conclusion

That’s all I have for you. Hopefully you learned something new, and you use later end up using this animation to liven up your own websites. I’m also currently using this animation on my website :)

If you’d like to see more design, a11y and related articles on my blog - do let me know. I’m eager to hear your feedback.

Enjoy the rest of your day 👋