profile picture of Olivier Larose

Olivier Larose

January 4, 2024

/

Beginner

/

Short

Parallax Scroll

2 Ways to Make a Scroll Parallax in React

A website tutorial on how to make a parallax scroll in React using GSAP or Framer Motion. All inside a Next.js application.

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.

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.
  • We will also use GSAP for the animation, so we can run npm i gsap.
  • We will use the Lenis Scroll for the smooth scrolling, so we can run npm i @studio-freight/lenis.

GSAP vs Framer Motion

There are big debates about using GSAP and Framer Motion inside a React app. I think both are great and they have their respective strengths. I believe it depends on the project, even tho I slightly prefer Framer Motion for most cases when using React.

So in this tutorial I'll remake the same animations using both GSAP and Framer Motion, so you can draw your own conclusions.

Rendering the layout

The layout will be approximately the same for GSAP and Framer Motion, but it's not the focus of this tutorial so I'll go rapidely over it.

/compone...

page.mod...

1

'use client';

2

import styles from '../../app/page.module.scss';

3

import Picture1 from '../../../public/medias/1.jpg';

4

import Picture2 from '../../../public/medias/2.jpg';

5

import Picture3 from '../../../public/medias/3.jpg';

6

import Image from "next/image";

7

8

const word = "with gsap";

9

10

export default function Index() {

11

const images = [Picture1, Picture2, Picture3];

12

return (

13

<div className={styles.container}>

14

<div className={styles.body}>

15

<h1>Parallax</h1>

16

<h1>Scroll</h1>

17

<div className={styles.word}>

18

<p>

19

{

20

word.split("").map((letter, i) => {

21

return <span key={`l_${i}`}>{letter}</span>

22

})

23

}

24

</p>

25

</div>

26

</div>

27

<div className={styles.images}>

28

{

29

images.map( (image, i) => {

30

return <div key={`i_${i}`} className={styles.imageContainer}>

31

<Image

32

src={image}

33

placeholder="blur"

34

alt="image"

35

fill

36

/>

37

</div>

38

})

39

}

40

</div>

41

</div>

42

)

43

}

We should have something like this

Screenshot of the HTML and CSS results

Parallax with GSAP

Now that the layout is done, we can add in GSAP to create the parallax effect.

The first important thing with GSAP is to add a timeline and inside of it create a ScrollTrigger. There are multiple ways of doing this, but I find using a timeline is the cleanest way of doing it. That way you have a single ScrollTrigger instance for multiple animations.

GSAP Timeline with ScrollTrigger

1

const tl = gsap.timeline({

2

scrollTrigger: {

3

trigger: container.current,

4

start: "top bottom",

5

end: "bottom top",

6

scrub: true,

7

},

8

})
  • Scrub: true is used to link the animations directly to the scrollbar

Then we can add all the different animations to the timeline.

Timeline animations

1

tl.to(title1.current, {y: -50}, 0)

2

3

//or with multiple refs

4

lettersRef.current.forEach((letter, i) => {

5

tl.to(letter, {

6

top: Math.floor(Math.random() * -75) - 25,

7

}, 0)

8

})
  • The 0 is added as a parameter to specify that the animations inside the timeline should happen at the same time.

Putting it all together

/components/GSAP

1

'use client';

2

import { useLayoutEffect, useRef } from "react";

3

import styles from '../../app/page.module.scss';

4

import gsap from 'gsap';

5

import { ScrollTrigger } from 'gsap/ScrollTrigger';

6

import Picture1 from '../../../public/medias/1.jpg';

7

import Picture2 from '../../../public/medias/2.jpg';

8

import Picture3 from '../../../public/medias/3.jpg';

9

import Image from "next/image";

10

gsap.registerPlugin(ScrollTrigger)

11

12

const word = "with gsap";

13

const images = [Picture1, Picture2, Picture3];

14

15

export default function Index() {

16

const container = useRef(null);

17

const title1 = useRef(null);

18

const lettersRef = useRef([])

19

const imagesRef = useRef([])

20

21

useLayoutEffect( () => {

22

const context = gsap.context( () => {

23

const tl = gsap.timeline({

24

scrollTrigger: {

25

trigger: container.current,

26

start: "top bottom",

27

end: "bottom top",

28

scrub: true,

29

},

30

})

31

.to(title1.current, {y: -50}, 0)

32

.to(imagesRef.current[1], {y: -150}, 0)

33

.to(imagesRef.current[2], {y: -255}, 0)

34

lettersRef.current.forEach((letter, i) => {

35

tl.to(letter, {

36

top: Math.floor(Math.random() * -75) - 25,

37

}, 0)

38

})

39

40

})

41

return () => context.revert();

42

}, [])

43

44

return (

45

<div ref={container} className={styles.container}>

46

<div className={styles.body}>

47

<h1 ref={title1}>Parallax</h1>

48

<h1>Scroll</h1>

49

<div className={styles.word}>

50

<p>

51

{

52

word.split("").map((letter, i) => {

53

return <span key={`l_${i}`} ref={el => lettersRef.current[i] = el}>{letter}</span>

54

})

55

}

56

</p>

57

</div>

58

</div>

59

<div className={styles.images}>

60

{

61

images.map( (image, i) => {

62

return <div key={`i_${i}`} ref={el => imagesRef.current[i] = el} className={styles.imageContainer}>

63

<Image

64

src={image}

65

placeholder="blur"

66

alt="image"

67

fill

68

/>

69

</div>

70

})

71

}

72

</div>

73

</div>

74

)

75

}
  • Here I'm using the useLayoutEffect because it is executed before the DOM is painted, in most cases that's what you want when creating a GSAP animation to avoid flashes.
  • I'm also using the gsap.context to collect all animations in one place that I can then kill in the return function of the useLayoutEffect.

We should have something like this

Parallax with Framer Motion

Now I'll do the same thing but Framer Motion. it's quite similar to the GSAP implementation, I don't have a clear winner but I do believe the Framer Motion implementation is a bit more clean and readable.

The first thing we need to use is the useScroll hook:

useScroll hook

1

const { scrollYProgress } = useScroll({

2

target: container,

3

offset: ['start end', 'end start']

4

})
  • Here we can track the position of the target inside the window. The scrollYProgress is a value between 0 and 1 depending on that progress.

Then we can use the useTransform hook to transform the value of the scrollYProgress into another value.

useTransform hook

1

const sm = useTransform(scrollYProgress, [0, 1], [0, -50]);
  • Here sm will be 0 when the scrollYProgress is 0. It will be 50 when the scrollYProgress is 1. It will also take all the values in between those two values.

Putting it all together

/components/FramerMotion

1

'use client';

2

import { useRef } from "react";

3

import styles from '../../app/page.module.scss';

4

import Picture1 from '../../../public/medias/4.jpg';

5

import Picture2 from '../../../public/medias/5.jpg';

6

import Picture3 from '../../../public/medias/6.jpg';

7

import Image from "next/image";

8

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

9

10

const word = "with framer-motion";

11

12

export default function Index() {

13

const container = useRef(null);

14

const { scrollYProgress } = useScroll({

15

target: container,

16

offset: ['start end', 'end start']

17

})

18

const sm = useTransform(scrollYProgress, [0, 1], [0, -50]);

19

const md = useTransform(scrollYProgress, [0, 1], [0, -150]);

20

const lg = useTransform(scrollYProgress, [0, 1], [0, -250]);

21

22

const images = [

23

{

24

src: Picture1,

25

y: 0

26

},

27

{

28

src: Picture2,

29

y: lg

30

},

31

{

32

src: Picture3,

33

y: md

34

}

35

];

36

37

return (

38

<div ref={container} className={styles.container}>

39

<div className={styles.body}>

40

<motion.h1 style={{y: sm}}>Parallax</motion.h1>

41

<h1>Scroll</h1>

42

<div className={styles.word}>

43

<p>

44

{

45

word.split("").map((letter, i) => {

46

const y = useTransform(scrollYProgress, [0, 1], [0, Math.floor(Math.random() * -75) - 25])

47

return <motion.span style={{top: y}} key={`l_${i}`} >{letter}</motion.span>

48

})

49

}

50

</p>

51

</div>

52

</div>

53

<div className={styles.images}>

54

{

55

images.map( ({src, y}, i) => {

56

return <motion.div style={{y}} key={`i_${i}`} className={styles.imageContainer}>

57

<Image

58

src={src}

59

placeholder="blur"

60

alt="image"

61

fill

62

/>

63

</motion.div>

64

})

65

}

66

</div>

67

</div>

68

)

69

}

We should have something like this:

Wrapping up

That's it for this animation!

Hope you liked the comparison between the two libraries. I find it useful to see how two identical animations can be made with 2 different libraries when having to make a choice between them.

-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