Accessible Wavy Text Animation using React Hooks and Framer Motion

We will learn to use Framer Motion with React hooks, next to that we animate some text and keep it accessible!

5 min read
by Mees Rutten | Wed Feb 17 2021

In this article i'll recreate a wavy text animation we built with React Hooks and GSAP v3 but with Framer Motion.
If you would like to see the GSAP tutorial, check it out here.

For the example I created my own small text split function. This example should be usable in React frameworks like Gatsby, Next.js, etc.

Image of wavy text

Tech stack

  • React for markup, templating, routing, etc.
  • Framer Motion for animation
  • EmotionCSS to write normal CSS, nest selectors and componentize styles

Goal

We want to create text that animates on first-load. The text should be split for each letter, then animate once smoothly.

CSS

We create some global CSS:

1/* @import for demo purposes. Don't use it in prod as its bad for performance */
2@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
3
4body {
5 margin: 0;
6 background: #7700ff;
7}
8
9.App {
10 font-family: poppins;
11 text-align: center;
12 background: radial-gradient(
13 farthest-side at bottom left,
14 rgba(255, 0, 255, 0.5),
15 transparent
16 ), radial-gradient(farthest-corner at bottom right, rgba(255, 50, 50, 0.5), transparent
17 400px);
18
19 position: relative;
20
21 width: 100%;
22 min-height: 100vh;
23
24 margin: 0;
25 padding: 0;
26
27 display: flex;
28 justify-content: center;
29 align-items: center;
30}

And we make the text look pretty with some css. The font-size value is based on a clamp function used in WebGL. The function creates a range between 1600px and 320px screenwidth. Within this range it calculates a font-size between 80px and 32px.
The motion.h2 means that we use the motion component from Framer Motion and tell it to use the h2 HTML element.

1const StyledTitleElement = styled(motion.h2)`
2 font-size: calc(32px + (80 - 32) * ((100vw - 320px) / (1600 - 320)));
3 line-height: calc(32px + (80 - 32) * ((100vw - 320px) / (1600 - 320)));
4
5 font-family: poppins;
6 text-transform: uppercase;
7
8 position: relative;
9 display: inline-block;
10 max-width: 100%;
11
12 word-break: break-word;
13 z-index: 10;
14
15 color: white;
16`;

Using hooks and writing markup

I'm using a timeout that updates a state inView. When inView updates it triggers the useEffect hook and replays the animation, resulting in a loop.

1export default function App() {
2 // You can toggle this with intersection observer.
3 const [inView, updateInView] = useState(false);
4
5 // Don't do this in production :)
6 useEffect(() => {
7 let timeout = setTimeout(() => {
8 updateInView(!inView);
9 }, 2000);
10
11 return () => clearTimeout(timeout);
12 }, [inView]);
13
14 return (
15 <main className="App">
16 <AnimatedTitle currentInView={inView}>level30wizards.com</AnimatedTitle>
17 </main>
18 );
19}

Animating with Framer Motion

To start animating with Framer Motion, we need to understand some jargon.
Framer Motion uses variants to distinguish between the initial state, the animate state and the exit state.
It looks like this:

1<StyledTitleElement
2 {...props}
3 variants={letterContainerVariants}
4 initial={"before"}
5 animate={"after"}
6 exit={"before"}
7 key={children}
8 aria-label={children} // people with assistive tech don't feel like hearing individual letters ;)
9 //aria-live="polite" add this if you dynamically show the element
10>

Framer Motion uses variants to distinguish between the initial state, the animate state and the exit state.

Variants

Variants are pretty much just JavaScript objects that contain information for the animation.
The letterContainerVariants are set on the parent element that contains all the individual letters.
The letterVariants are set on, you guessed it, the individual letters.

1// Add staggering effect to the children of the container
2export const letterContainerVariants = {
3 before: { transition: { staggerChildren: 0.015 } }, // When the letters show they stagger with a duration of 0.015 seconds
4 after: { transition: { staggerChildren: 0.03 } }, // When the letters hide they stagger with a duration of 0.03 seconds
5};
6
7// Variants for animating each letter
8export const letterVariants = {
9 // before state or initial
10 before: {
11 opacity: 0, // invisible!
12 y: 20, // a bit lower than final position
13 transition: {
14 type: 'spring', //physical spring transition
15 damping: 12, //how quickly the spring slows down
16 stiffness: 200, // how "stiff" the spring is
17 },
18 },
19 // after state or exit
20 after: {
21 opacity: 1,
22 y: 0,
23 transition: {
24 type: 'spring',
25 damping: 12,
26 stiffness: 200,
27 },
28 },
29};

Splitting words in React

1{
2 {
3 /* split sentences into words */
4 }
5 children.split(' ').map((word: string, wordI: number) => (
6 <div
7 key={`word-${word}-${wordI}`}
8 style={{
9 display: 'inline-block',
10 }}
11 >
12 {/* split words into letters */}
13 {Array.from(word).map((letter, index) => (
14 <motion.span // we need motion because we will add a Framer Motion animation variant to it.
15 key={`${index}-${letter}`}
16 style={{
17 position: 'relative',
18 display: 'inline-block',
19 width: 'auto',
20 }}
21 variants={letterVariants} // variant for the individual letter
22 >
23 {/* letter or space */}
24 {letter === ' ' ? '\u00A0' : letter}
25 </motion.span>
26 ))}
27 {'\u00A0'}
28 </div>
29 ));
30}

Demo

You can check out the demo here:

Final words

That's it!
You can link this animation to an Intersection Observer to animate while scrolling, or keep it like this to animate when someone navigates.
If you want to see it in production, visit: Level30Wizards


Thanks for reading!

I hope someone somewhere learned something via this post! If you did, please consider sharing the article.

Special thanks to @moniac for sharing this template!

by Mees Rutten @

All rights reserved