profile picture of Olivier Larose

Olivier Larose

January 13, 2024

/

Beginner

/

Short

Zoom Parallax

How to Make a Zoom Parallax with React using Next.js and Framer Motion

A Smooth Scroll Parallax animation featuring a zoom with a sticky container. Made with React, Framer Motion and Next js. Picture by Matthias Leidinger. Inspired by https://www.exoape.com/work/plugged-live-shows

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

Adding a smooth scroll

We can make this animation first by first adding a smooth scroll. There are many ways of doing this and it's not necessary for the animation, but I personally like it.

For that, I'll use Lenis Scroll:

Smooth Scroll

1

useEffect( () => {

2

const lenis = new Lenis()

3

4

function raf(time) {

5

lenis.raf(time)

6

requestAnimationFrame(raf)

7

}

8

9

requestAnimationFrame(raf)

10

},[])

The Basics

Here's the basics of making this animations. We want to have a main container with a long scroll, something like 300vh and inside of it have a sticky container of 100vh that will stick throughout the whole length of its parent.

Then we can track the progress of the scroll and scale all our divs consequently.

Scale based on the scroll

1

const { scrollYProgress } = useScroll({

2

target: container,

3

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

4

})

5

6

const scale4 = useTransform(scrollYProgress, [0, 1], [1, 4]);

Here's how it would look for the main image at the center.

ZoomPara...

ZoomPara...

1

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

2

import Picture1 from '../../../public/images/1.jpeg';

3

import Image from 'next/image';

4

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

5

import { useRef } from 'react';

6

7

export default function Index() {

8

9

const container = useRef(null);

10

const { scrollYProgress } = useScroll({

11

target: container,

12

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

13

})

14

15

const scale = useTransform(scrollYProgress, [0, 1], [1, 4]);

16

17

return (

18

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

19

<div className={styles.sticky}>

20

<motion.div style={{scale}} className={styles.el}>

21

<div className={styles.imageContainer}>

22

<Image

23

src={Picture1}

24

fill

25

alt="image"

26

placeholder='blur'

27

/>

28

</div>

29

</motion.div>

30

</div>

31

</div>

32

)

33

}
  • Since the image-container has a width and height of 25vw and 25vh, all we have to do is scale it from 1 to 4 to make the image take the full screen at any screen size.

We should have something like this:

Adding the other images

To make the code extra clean, I create an array of images with different scale values. I can then simply loop that array to render all the images.

Array of images

1

const scale4 = useTransform(scrollYProgress, [0, 1], [1, 4]);

2

const scale5 = useTransform(scrollYProgress, [0, 1], [1, 5]);

3

const scale6 = useTransform(scrollYProgress, [0, 1], [1, 6]);

4

const scale8 = useTransform(scrollYProgress, [0, 1], [1, 8]);

5

const scale9 = useTransform(scrollYProgress, [0, 1], [1, 9]);

6

7

const pictures = [

8

{

9

src: Picture1,

10

scale: scale4

11

},

12

{

13

src: Picture2,

14

scale: scale5

15

},

16

{

17

src: Picture3,

18

scale: scale6

19

},

20

...

21

]

22

Now that we have a clean structure, I can simply loop the array and render all my images. Most of the work is then done in CSS to place all the images correctly.

ZoomPara...

ZoomPara...

1

return (

2

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

3

<div className={styles.sticky}>

4

{

5

pictures.map( ({src, scale}, index) => {

6

return <motion.div key={index} style={{scale}} className={styles.el}>

7

<div className={styles.imageContainer}>

8

<Image

9

src={src}

10

fill

11

alt="image"

12

placeholder='blur'

13

/>

14

</div>

15

</motion.div>

16

})

17

}

18

</div>

19

</div>

20

)
  • To avoid making the divs overlap on top of each others as they are scaling, I'm actually scaling the el and not the image-container. That way, the scaling is more natural and is keeping it's original layout.

We should have something like this:

Wrapping up

Quick, clean and easy like this!

Hope you liked the animation, it's a good trick to scale the parent to keep the original layout when doing a zoom parallax like this. Hope you learned something!

-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