Initializing the project
Let's start the project by creating a Next.js application. We can do that by running npx create-next-app@latest client
inside of a terminal.
Adding the HTML and CSS
We can delete everything in the page.js
, global.css
and page.module.css
and add our own HTML and CSS, to start with a nice blank application.
2
import styles from './page.module.css'
5
"It is a long established fact",
6
"that a reader will be distracted",
7
"by the readable content of a page",
8
"when looking at its layout."
11
export default function Home() {
13
<div className={styles.container}>
23
export function MaskText() {
25
<div className={styles.body}>
27
phrases.map( (phrase, index) => {
28
return <div key={index} className={styles.lineMask}>
Nothing too complicated here, we map the phrases and return a div
for each of of them.
We should have something like this
Detect when in view
Before animating the text, we need to find a way to detect when the text is inside the viewport, so that only then we will animate the text.
To do that, we're going to use the useInView hook from Framer-Motion. To install it, we can run npm i framer-motion
1
import { useInView } from 'react-intersection-observer';
3
export function MaskText() {
5
const body = useRef(null);
6
const isInView = useInView(body, {once: true, margin: "75%"})
9
<div ref={body} className={styles.body}>
margin:"75%"
We wait until the body is visible by 75% before considering it to be in view.
With that in place, we can now detect when the text is in view, and animate in consequence.
Animate in
For the animation, we will use Framer-Motion, but you could use anything like GSAP or even pure CSS.
We can create a new animation variant that will manipule the y attribute of our the text.
1
import { useRef } from 'react';
2
import { useInView, motion } from 'framer-motion';
5
export function MaskText() {
7
const body = useRef(null);
8
const isInView = useInView(body, {once: true, margin: "-75%"})
12
enter: i => ({y: "0", transition: {duration: 0.75, ease: [0.33, 1, 0.68, 1], delay: 0.075 * i}})
16
<div ref={body} className={styles.body}>
18
phrases.map( (phrase, index) => {
19
return <div key={index} className={styles.lineMask}>
20
<motion.p custom={index} variants={animation} initial="initial" animate={isInView ? "enter" : ""}>{phrase}</motion.p>
Here's a couple of important things to note:
- We add
overflow:hidden
to the div that encapsulates the div, so that all children will be invisibile outside of its bounds. - We create a new animation variant that initially set the
y
property of the text at 100%.
- When the text comes in view,
isInView
becomes true
, triggering the animation and thus setting the y
property at 0%.
- The
custom
attribute is used to transfer the index and create a delay in the animation between the different phrases.
And that's it! You should see this:
Wrapping up
That was it for this animation, it's a very common animation that I find inside of a lot of awwwards winning animation. It's a pretty simple concept, but good to have inside of our toolbox.
Hope you learned a lot :)
-Oli