DEV Community

Cover image for Create a Satisfying Wavy Text Animation With Framer Motion
Harsh Singh
Harsh Singh

Posted on

Create a Satisfying Wavy Text Animation With Framer Motion

Hello there! It's been a while. Although now that my AP Computer Science exams are over, and summer is almost here (30 days of school left), 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.

npx create-react-app wavy-text --template typescript
cd wavy-text
Enter fullscreen mode Exit fullscreen mode

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

yarn add framer-motion
# npm i framer-motion
Enter fullscreen mode Exit fullscreen mode

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

import "./styles.css";
import WavyText from "./WavyText";

export default function App() {
  return (
    <div className="App">
      <h1>Awesome Wavy Text!</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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 first import what we'll need for this project and configure our props for the component.

import { FC } from "react";
import { motion, Variants, HTMLMotionProps } from "framer-motion";

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

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.

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

}
Enter fullscreen mode Exit fullscreen mode

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.

const letters = Array.from(text);
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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

<motion.h1
  style={{ display: "flex", overflow: "hidden" }}
  variants={container}
  initial="hidden"
  animate="show"
  {...props}
>
Enter fullscreen mode Exit fullscreen mode

...and in our child component:

<motion.span key={index} variants={child}>
Enter fullscreen mode Exit fullscreen mode

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:

// import WavyText from "./WavyText";
// ...

<WavyText text="Awesome Wavy Text!" />
Enter fullscreen mode Exit fullscreen mode

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 👋

Discussion (0)