profile picture of Olivier Larose

Olivier Larose

November 14, 2023

/

Beginner

/

Short

Cards Parallax

Build a Smooth Scroll Cards Parallax with Framer Motion and Next.js

A website Smooth Scroll Cards Parallax animation tutorial featuring Lenis Scroll, Framer Motion all inside a Next.js application. Inspired by many awwwards winning websites.

Live DemoSource codeVideo Tutorial
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.

Rendering the Sticky Cards

The first thing I set up to render the cards in a more efficient way, is to use an array of object. That way I can simply map it and return everything I need. Here's how it looks:

src/data.js

1

export const projects = [

2

{

3

title: "Matthias Leidinger",

4

description: "Originally hailing from Austria, Berlin-based photographer Matthias Leindinger is a young creative brimming with talent and ideas.",

5

src: "rock.jpg",

6

link: "https://www.ignant.com/2023/03/25/ad2186-matthias-leidingers-photographic-exploration-of-awe-and-wonder/",

7

color: "#BBACAF"

8

},

9

{

10

title: "Clément Chapillon",

11

description: "This is a story on the border between reality and imaginary, about the contradictory feelings that the insularity of a rocky, arid, and wild territory provokes”—so French photographer Clément Chapillon describes his latest highly captivating project Les rochers fauves (French for ‘The tawny rocks’).",

12

src: "tree.jpg",

13

link: "https://www.ignant.com/2022/09/30/clement-chapillon-questions-geographical-and-mental-isolation-with-les-rochers-fauves/",

14

color: "#977F6D"

15

},

16

{

17

title: "Zissou",

18

description: "Though he views photography as a medium for storytelling, Zissou’s images don’t insist on a narrative. Both crisp and ethereal, they’re encoded with an ambiguity—a certain tension—that lets the viewer find their own story within them.",

19

src: "water.jpg",

20

link: "https://www.ignant.com/2023/10/28/capturing-balis-many-faces-zissou-documents-the-sacred-and-the-mundane-of-a-fragile-island/",

21

color: "#C2491D"

22

},

23

...

24

]

Page.js

Then I can use that array inside the page.js to render all the cards:

app/page...

app/page...

1

'use client';

2

import styles from './page.module.css'

3

import { projects } from '../data';

4

import Card from '../components/Card';

5

6

export default function Home() {

7

return (

8

<main className={styles.main}>

9

{

10

projects.map( (project, i) => {

11

return <Card key={`p_${i}`} {...project} i={i}/>

12

})

13

}

14

</main>

15

)

16

}
Here I also externalize the code inside a new Card component.

Card Component

The card component is simple HTML and CSS for now, they each take 100vh of height and they have a sticky position to make them stick when they reach the top of the window.

componen...

componen...

1

'use client'

2

import Image from 'next/image';

3

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

4

5

const Card = ({title, description, src, url, color, i}) => {

6

7

return (

8

<div className={styles.cardContainer}>

9

<div

10

className={styles.card}

11

style={{backgroundColor: color, top:`calc(-5vh + ${i * 25}px)`}}

12

>

13

<h2>{title}</h2>

14

<div className={styles.body}>

15

<div className={styles.description}>

16

<p>{description}</p>

17

<span>

18

<a href={url} target="_blank">See more</a>

19

<svg width="22" height="12" viewBox="0 0 22 12" fill="none" xmlns="http://www.w3.org/2000/svg">

20

<path d="M21.5303 6.53033C21.8232 6.23744 21.8232 5.76256 21.5303 5.46967L16.7574 0.696699C16.4645 0.403806 15.9896 0.403806 15.6967 0.696699C15.4038 0.989592 15.4038 1.46447 15.6967 1.75736L19.9393 6L15.6967 10.2426C15.4038 10.5355 15.4038 11.0104 15.6967 11.3033C15.9896 11.5962 16.4645 11.5962 16.7574 11.3033L21.5303 6.53033ZM0 6.75L21 6.75V5.25L0 5.25L0 6.75Z" fill="black"/>

21

</svg>

22

</span>

23

</div>

24

25

<div className={styles.imageContainer}>

26

<div className={styles.inner}>

27

<Image

28

fill

29

src={`/images/${src}`}

30

alt="image"

31

/>

32

</div>

33

</div>

34

</div>

35

</div>

36

</div>

37

)

38

}

39

40

export default Card
  • Line 11: A dynamic top position is set depending on the index of each cards, creating a simple stacking effect. Also note that's how the color of each card is set.

We should have something like this:

On Scroll Image Scaling

To scale each image on scroll, I'll use Framer Motion and the useScroll hook and the useTransform The general concept is the more we scroll, the more I want to scale the image, here's how I did it:

components/Card/index.jsx

1

...

2

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

3

import { useRef } from 'react';

4

5

const Card = ({title, description, src, url, color, i}) => {

6

7

const container = useRef(null);

8

const { scrollYProgress } = useScroll({

9

target: container,

10

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

11

})

12

const imageScale = useTransform(scrollYProgress, [0, 1], [2, 1])

13

14

return (

15

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

16

<div

17

className={styles.card}

18

style={{backgroundColor: color, top:`calc(-5vh + ${i * 25}px)`}}

19

>

20

...

21

<div className={styles.imageContainer}>

22

<motion.div

23

className={styles.inner}

24

style={{scale: imageScale}}

25

>

26

<Image

27

fill

28

src={`/images/${src}`}

29

alt="image"

30

/>

31

</motion.div>

32

</div>

33

</div>

34

</div>

35

</div>

36

)

37

}

38

39

export default Card
  • Line 8-11: Here I use the useScroll hook to track whenever the card enters the viewport. It returns a value between 0 and 1 depending, starting at the intersection of the top of the card container and the end of the window 'start end' and ending at the intersection of the top of the card container and the start of the window 'start start'
  • Line 12: I use the useTransform hook to transform the value of the progress of the scroll [0,1] into a new value that I'll use for the scale of the image [2,1]

We should have something like this:

Cards Parallax

To create the parallax, I use the same hooks as I did for the image scaling. However, instead of being localized inside a Card component, it will be global for the whole animation. So I'll use the useScroll inside the page.js for that.

page.js

1

'use client';

2

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

3

import { projects } from '../data';

4

import Card from '../components/Card';

5

import { useScroll } from 'framer-motion';

6

import { useRef } from 'react';

7

8

export default function Home() {

9

const container = useRef(null);

10

const { scrollYProgress } = useScroll({

11

target: container,

12

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

13

})

14

15

return (

16

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

17

{

18

projects.map( (project, i) => {

19

const targetScale = 1 - ( (projects.length - i) * 0.05);

20

return <Card key={`p_${i}`} i={i} {...project} progress={scrollYProgress} range={[i * .25, 1]} targetScale={targetScale}/>

21

})

22

}

23

</main>

24

)

25

}
  • Line 10: Tracks the whole animation, it returns a value between 0 and 1, 0 being when the top of the main intersects with the top of the window 'start start' and 1 being when the end of the main intersects with the end of the window 'end end'.
  • Line 19: A scale value that is given to the card, the first cards should have a higher target scale than the last ones.

Adjusting the Card Component

components/Card/index.jsx

1

const Card = ({i, title, description, src, url, color, progress, range, targetScale}) => {

2

...

3

const scale = useTransform(progress, range, [1, targetScale]);

4

5

return (

6

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

7

<motion.div

8

className={styles.card}

9

style={{backgroundColor: color, scale, top:`calc(-5vh + ${i * 25}px)`}}

10

>

11

...

12

</div>

13

</motion.div>

14

</div>

15

)

16

}

17

18

export default Card

19

  • Line 3: Here the scaling of each card is dictated by the progress of the scroll and the targetScale initialized inside the page.js.

We should have something like this:

Adding a Smooth Scroll

The final touch of this animation is to make everything smooth. Since the animation is fixed to the scroll, we can make it smooth by smoothing out the scroll. For that I use the Lenis Scroll

page.js

1

...

2

import { useRef, useEffect } from 'react';

3

import Lenis from '@studio-freight/lenis'

4

5

export default function Home() {

6

...

7

useEffect( () => {

8

const lenis = new Lenis()

9

10

function raf(time) {

11

lenis.raf(time)

12

requestAnimationFrame(raf)

13

}

14

15

requestAnimationFrame(raf)

16

})

17

18

return (

19

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

20

...

21

</main>

22

)

23

}

24

We should have something like this

Wrapping up

That's it for this animation!

A very common animation, found in multiple awwwards website, and this is a nice, simple approach to it! 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