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!
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.
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');34body {5 margin: 0;6 background: #7700ff;7}89.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 transparent16 ), radial-gradient(farthest-corner at bottom right, rgba(255, 50, 50, 0.5), transparent17 400px);1819 position: relative;2021 width: 100%;22 min-height: 100vh;2324 margin: 0;25 padding: 0;2627 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)));45 font-family: poppins;6 text-transform: uppercase;78 position: relative;9 display: inline-block;10 max-width: 100%;1112 word-break: break-word;13 z-index: 10;1415 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);45 // Don't do this in production :)6 useEffect(() => {7 let timeout = setTimeout(() => {8 updateInView(!inView);9 }, 2000);1011 return () => clearTimeout(timeout);12 }, [inView]);1314 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<StyledTitleElement2 {...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 element10>
Framer Motion uses
variants
to distinguish between theinitial
state, theanimate
state and theexit
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 container2export const letterContainerVariants = {3 before: { transition: { staggerChildren: 0.015 } }, // When the letters show they stagger with a duration of 0.015 seconds4 after: { transition: { staggerChildren: 0.03 } }, // When the letters hide they stagger with a duration of 0.03 seconds5};67// Variants for animating each letter8export const letterVariants = {9 // before state or initial10 before: {11 opacity: 0, // invisible!12 y: 20, // a bit lower than final position13 transition: {14 type: 'spring', //physical spring transition15 damping: 12, //how quickly the spring slows down16 stiffness: 200, // how "stiff" the spring is17 },18 },19 // after state or exit20 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 <div7 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 letter22 >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.
Other posts you might like
How to develop a Web AR Facefilter with React and ThreeJS / React Three Fiber in 2021
September 09, 2021For the 3 year anniversary of 🧙🏼‍♂️ Level30Wizards we wanted to build something that's related…
Creating scroll snapping blocks with GSAP v3 animations
July 01, 2020Setting up some HTML So firstly I want to say something about using a lot of div elements. Stop…
Beginner guide to web animation to get started
June 30, 2020A Creative Front-End Developer or Motion Developer will make a UI or concept come to life on the web…