profile picture of Olivier Larose

Olivier Larose

November 23, 2023

/

Beginner

/

Short

Text Gradient Scroll Opacity v2

Build a Text Scroll Gradient Opacity Effect using Nextjs and Framer Motion

A web animation tutorial featuring a gradient text scroll opacity effect using Nextjs and Framer Motion. In this tutorial I animate a paragraph work by word modifying the opacity in scroll. Inspired by many awwwards websites.

Live DemoSource code
background video

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.

  • The useScroll hook returns a value between 0 and 1 depending on the progress of the scroll. We can directly use that value to modify the opacity.
  • useScroll hook

    1

    const container = useRef(null);

    2

    const { scrollYProgress } = useScroll({

    3

    target: container,

    4

    offset: ["start 0.9", "start 0.25"]

    5

    })

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:

componen...

componen...

1

import { motion, useScroll, useTransform } from 'framer-motion';

2

import React, { useRef } from 'react';

3

import styles from './style.module.scss';

4

5

export default function Paragraph({paragraph}) {

6

7

const container = useRef(null);

8

const { scrollYProgress } = useScroll({

9

target: container,

10

offset: ["start 0.9", "start 0.25"]

11

})

12

13

return (

14

<motion.p

15

ref={container}

16

className={styles.paragraph}

17

style={{opacity: scrollYProgress}}

18

>

19

{paragraph}

20

</motion.p>

21

)

22

}

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

Mapping the paragraph

1

const words = paragraph.split(" ")

2

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>

7

})

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.

Word Component

1

const Word = ({children, progress, range}) => {

2

const opacity = useTransform(progress, range, [0, 1])

3

return (

4

<span className={styles.word}>

5

<span className={styles.shadow}>{children}</span>

6

<motion.span style={{opacity: opacity}}>{children}</motion.span>

7

</span>

8

)

9

}

The range is transformed into a value between 0 and 1 and used as an opacity.

We should have something like this:

componen...

componen...

1

import { motion, useScroll, useTransform } from 'framer-motion';

2

import React, { useRef } from 'react';

3

import styles from './style.module.scss';

4

5

export default function Paragraph({paragraph}) {

6

7

const container = useRef(null);

8

const { scrollYProgress } = useScroll({

9

target: container,

10

offset: ["start 0.9", "start 0.25"]

11

})

12

13

const words = paragraph.split(" ")

14

return (

15

<p

16

ref={container}

17

className={styles.paragraph}

18

>

19

{

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>

24

})

25

}

26

</p>

27

)

28

}

29

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>

35

</span>

36

}

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.

Mapping the words

1

const Word = ({children, progress, range}) => {

2

const amount = range[1] - range[0]

3

const step = amount / children.length

4

return (

5

<span className={styles.word}>

6

{

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>

11

})

12

}

13

</span>

14

)

15

}
  • 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.

Character component

1

const Char = ({children, progress, range}) => {

2

const opacity = useTransform(progress, range, [0,1])

3

return (

4

<span>

5

<span className={styles.shadow}>{children}</span>

6

<motion.span style={{opacity: opacity}}>{children}</motion.span>

7

</span>

8

)

9

}

We should have something like this:

componen...

componen...

1

import { useScroll, useTransform, motion } from 'framer-motion';

2

import React, { useRef } from 'react';

3

import styles from './style.module.scss';

4

5

export default function Paragraph({paragraph}) {

6

7

const container = useRef(null);

8

const { scrollYProgress } = useScroll({

9

target: container,

10

offset: ["start 0.9", "start 0.25"]

11

})

12

13

const words = paragraph.split(" ")

14

return (

15

<p

16

ref={container}

17

className={styles.paragraph}

18

>

19

{

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>

24

})

25

}

26

</p>

27

)

28

}

29

30

const Word = ({children, progress, range}) => {

31

const amount = range[1] - range[0]

32

const step = amount / children.length

33

return (

34

<span className={styles.word}>

35

{

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>

40

})

41

}

42

</span>

43

)

44

}

45

46

const Char = ({children, progress, range}) => {

47

const opacity = useTransform(progress, range, [0,1])

48

return (

49

<span>

50

<span className={styles.shadow}>{children}</span>

51

<motion.span style={{opacity: opacity}}>{children}</motion.span>

52

</span>

53

)

54

}

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

Related Animations

image

June 2, 2024

Mask Section Transition

A website tutorial featuring a scroll animation using an SVG Mask to create a section transition, made with React, Framer Motion. Inspired by: https://axelvanhessche.com/. Pictures by Eric Asamoah, Inka and Niclas Lindergård, Daniel Ribar

image

May 25, 2024

Background Image Parallax

A website animation featuring a background image moving on scroll in a parallax motion, made with Framer Motion and React, inside a Next.js app. Inspired by: https://inkfishnyc.com/. Pictures by Matthias Leidinger

image

May 25, 2024

Text Parallax

A website animation featuring a Text Parallax with sliding text on scroll, made with Framer Motion and React, inside a Next.js app