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.
- We will use Sass for the stylesheets, so we can run
npm i sass
. - We will use Framer Motion for the animation, so we can run
npm i framer-motion
.
To illustrate this animation, I'll go step by step, animating a paragraph on scroll first, then moving to a word by word animation and then doing a character by character animation. Depending on your taste (I like the word by word the best), you can choose whichever implementation you prefer.
Animate a Paragraph
Animating a whole paragraph is the easiest approach, I'll use the useScroll hook from Framer Motion and animate the opacity on scroll.
Notes about the offset:
- The
offset
here specifies two intersections of the target and the container. In this case, the target is the paragraph and the container is the window by default. start
is the top of the target and 1 is the end of the window, so 0.9
is 10% before the end of the window. At that intersection, the scrollYProgress
will have a value of 0.- The second intersection is
"start 0.25"
which is the top of the target and 25% further than the top of the window. At that intersection, the scrollYProgress
will have a value of 1.
We should have something like this:
1
import { motion, useScroll, useTransform } from 'framer-motion';
2
import React, { useRef } from 'react';
3
import styles from './style.module.scss';
5
export default function Paragraph({paragraph}) {
7
const container = useRef(null);
8
const { scrollYProgress } = useScroll({
10
offset: ["start 0.9", "start 0.25"]
16
className={styles.paragraph}
17
style={{opacity: scrollYProgress}}
Animate Word by Word
The word by word will take the same concept as before for the scrollYProgress
, but now I'll separate each word into it's own component. I'll also create a range for each word, so they'll be animated one by one. For that I'll add in the useTransform hook to transform the range into an opacity.
- The
scrollYProgress
is the be the same as above
1
const words = paragraph.split(" ")
3
words.map( (word, i) => {
4
const start = i / words.length
5
const end = start + (1 / words.length)
6
return <Word key={i} progress={scrollYProgress} range={[start, end]}>{word}</Word>
The start
and the end
are dictacted by the position of the word inside the paragraph. If the word is at the beginning of the paragraph, then it will have an earlier start and end point.
1
const Word = ({children, progress, range}) => {
2
const opacity = useTransform(progress, range, [0, 1])
4
<span className={styles.word}>
5
<span className={styles.shadow}>{children}</span>
6
<motion.span style={{opacity: opacity}}>{children}</motion.span>
The range
is transformed into a value between 0 and 1 and used as an opacity.
We should have something like this:
1
import { motion, useScroll, useTransform } from 'framer-motion';
2
import React, { useRef } from 'react';
3
import styles from './style.module.scss';
5
export default function Paragraph({paragraph}) {
7
const container = useRef(null);
8
const { scrollYProgress } = useScroll({
10
offset: ["start 0.9", "start 0.25"]
13
const words = paragraph.split(" ")
17
className={styles.paragraph}
20
words.map( (word, i) => {
21
const start = i / words.length
22
const end = start + (1 / words.length)
23
return <Word key={i} progress={scrollYProgress} range={[start, end]}>{word}</Word>
30
const Word = ({children, progress, range}) => {
31
const opacity = useTransform(progress, range, [0, 1])
32
return <span className={styles.word}>
33
<span className={styles.shadow}>{children}</span>
34
<motion.span style={{opacity: opacity}}>{children}</motion.span>
Animate Character by Character
And finally we can add another layer by splitting the words by character. Then we also need to split the range of each words into a new range for each of their characters.
1
const Word = ({children, progress, range}) => {
2
const amount = range[1] - range[0]
3
const step = amount / children.length
5
<span className={styles.word}>
7
children.split("").map((char, i) => {
8
const start = range[0] + (i * step);
9
const end = range[0] + ((i + 1) * step)
10
return <Char key={`c_${i}`} progress={progress} range={[start, end]}>{char}</Char>
- To split the range of each words into a new range for each character, I calculate the
amount
and from it a step
depending on the length of the word.
1
const Char = ({children, progress, range}) => {
2
const opacity = useTransform(progress, range, [0,1])
5
<span className={styles.shadow}>{children}</span>
6
<motion.span style={{opacity: opacity}}>{children}</motion.span>
We should have something like this:
1
import { useScroll, useTransform, motion } from 'framer-motion';
2
import React, { useRef } from 'react';
3
import styles from './style.module.scss';
5
export default function Paragraph({paragraph}) {
7
const container = useRef(null);
8
const { scrollYProgress } = useScroll({
10
offset: ["start 0.9", "start 0.25"]
13
const words = paragraph.split(" ")
17
className={styles.paragraph}
20
words.map( (word, i) => {
21
const start = i / words.length
22
const end = start + (1 / words.length)
23
return <Word key={i} progress={scrollYProgress} range={[start, end]}>{word}</Word>
30
const Word = ({children, progress, range}) => {
31
const amount = range[1] - range[0]
32
const step = amount / children.length
34
<span className={styles.word}>
36
children.split("").map((char, i) => {
37
const start = range[0] + (i * step);
38
const end = range[0] + ((i + 1) * step)
39
return <Char key={`c_${i}`} progress={progress} range={[start, end]}>{char}</Char>
46
const Char = ({children, progress, range}) => {
47
const opacity = useTransform(progress, range, [0,1])
50
<span className={styles.shadow}>{children}</span>
51
<motion.span style={{opacity: opacity}}>{children}</motion.span>
Wrapping up
That's it for this animation!
Depending on your taste, you can choose whichever implementation you prefer. It's a classic animation that I've seen everywhere but it's mostly made with GSAP. So I hope you learned something from this Framer Motion implementation.
-Oli