profile picture of Olivier Larose

Olivier Larose

June 25, 2023

/

Intermediate

/

Medium

Smooth Scroll

Build a Smooth Scroll Landing Page using Nextjs, GSAP, Locomotive Scroll v5

A website tutorial on how to make a one pager landing page with a smooth scroll using Nextjs, GSAP and Locomotive Scroll v5

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.

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.

This landing page will be composed of 4 components:

  • Page Component: the parent, initializes the Locomotive Scroll and imports the other two components.
  • Intro Component: the first section, features a background image with a clip-path animation and a body with an animated title and image.
  • Description Component: the second section, features scroll animated paragraphs.
  • Projects Component: the last section, features a pinned image and a state-based project gallery.

The 3 components are created inside a /src/components/ folder.

The Page Component

The page component is the parent of the other 3 components. It also manages the smooth scroll which is very simply done with Locomotive Scroll v5

page.js

1

'use client';

2

import { useEffect } from 'react';

3

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

4

import Intro from '../components/Intro';

5

import Description from '../components/Description';

6

import Projects from '../components/Projects';

7

8

export default function Home() {

9

10

useEffect( () => {

11

(

12

async () => {

13

const LocomotiveScroll = (await import('locomotive-scroll')).default

14

const locomotiveScroll = new LocomotiveScroll();

15

}

16

)()

17

}, [])

18

19

return (

20

<main className={styles.main}>

21

<Intro />

22

<Description />

23

<Projects />

24

</main>

25

)

26

}

27

Note: The Locomotive Scroll is purely a client-side library, so we need to import it in an async way after the component mounts. If we don't do this, we will receive an error mentionning the window does not exist.

  • With that, we now have a native smooth scroll, amazing!

The Intro Component

The Intro is composed of 3 main elements:

  • The background image: animated with Scroll Trigger, using the clip-path property
  • The main image: animated with Scroll Trigger, using the height property and Locomotive Scroll with data-scroll-speed
  • The title: animated with Locomotive Scroll with data-scroll-speed

Intro/in...

Intro/st...

1

'use client';

2

import React, { useLayoutEffect, useRef } from 'react'

3

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

4

import Image from 'next/image';

5

import gsap from 'gsap';

6

import { ScrollTrigger } from 'gsap/ScrollTrigger';

7

8

export default function Index() {

9

10

const background = useRef(null);

11

const introImage = useRef(null);

12

const homeHeader = useRef(null);

13

14

useLayoutEffect( () => {

15

gsap.registerPlugin(ScrollTrigger);

16

17

const timeline = gsap.timeline({

18

scrollTrigger: {

19

trigger: document.documentElement,

20

scrub: true,

21

start: "top",

22

end: "+=500px",

23

},

24

})

25

26

timeline

27

.from(background.current, {clipPath: `inset(15%)`})

28

.to(introImage.current, {height: "200px"}, 0)

29

}, [])

30

31

return (

32

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

33

<div className={styles.backgroundImage} ref={background}>

34

<Image

35

src={'/images/background.jpeg'}

36

fill={true}

37

alt="background image"

38

priority={true}

39

/>

40

</div>

41

<div className={styles.intro}>

42

<div ref={introImage} data-scroll data-scroll-speed="0.3" className={styles.introImage}>

43

<Image

44

src={'/images/intro.png'}

45

alt="intro image"

46

fill={true}

47

priority={true}

48

/>

49

</div>

50

<h1 data-scroll data-scroll-speed="0.7">SMOOTH SCROLL</h1>

51

</div>

52

</div>

53

)

54

}

Notes about the code above:

  • A ScrollTrigger timeline is created from the top of the document to +=500px.
  • Line 27: The background is animated from clipPath: inset(15%) for the duration of the timeline.
  • Line 28: The main image height is reduced to 200px for the duration of the timeline.
  • Line 42 and 50: A parallax on the main image and the title is created using the data-scroll-speed from Locomotive Scroll.

Here's the result:

The Description Component

The Description component features an internal component called AnimatedText. An array of phrases is iterated to return that component.

Inside of it, a new ScrollTrigger is created from the top of each paragraphs and ends at +=400px. The left value and the opacity are adjusted during the duration of the scroll.

Descript...

Descript...

1

import React, { useLayoutEffect, useRef } from 'react'

2

import { ScrollTrigger } from 'gsap/ScrollTrigger';

3

import gsap from 'gsap';

4

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

5

6

const phrases = ["Los Flamencos National Reserve", "is a nature reserve located", "in the commune of San Pedro de Atacama", "The reserve covers a total area", "of 740 square kilometres (290 sq mi)"]

7

8

export default function Index() {

9

10

return (

11

<div className={styles.description} >

12

{

13

phrases.map( (phrase, index) => {

14

return <AnimatedText key={index}>{phrase}</AnimatedText>

15

})

16

}

17

</div>

18

)

19

}

20

21

function AnimatedText({children}) {

22

const text = useRef(null);

23

24

useLayoutEffect( () => {

25

gsap.registerPlugin(ScrollTrigger);

26

gsap.from(text.current, {

27

scrollTrigger: {

28

trigger: text.current,

29

scrub: true,

30

start: "0px bottom",

31

end: "bottom+=400px bottom",

32

},

33

opacity: 0,

34

left: "-200px",

35

ease: "power3.Out"

36

})

37

}, [])

38

39

return <p ref={text}>{children}</p>

40

}

Here's the result:

The Projects Component

The Projects component is simple HTML and CSS. The only thing we animate is a pin on the image.

There's also an internal state to track which project is current highlighted, which will dynamically change the src of the image.

Projects...

Projects...

1

import React, { useState, useLayoutEffect, useRef } from 'react'

2

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

3

import Image from 'next/image';

4

import gsap from 'gsap';

5

import { ScrollTrigger } from 'gsap/ScrollTrigger';

6

7

const projects = [

8

{

9

title: "Salar de Atacama",

10

src: "salar_de_atacama.jpg"

11

},

12

{

13

title: "Valle de la luna",

14

src: "valle_de_la_muerte.jpeg"

15

},

16

{

17

title: "Miscanti Lake",

18

src: "miscani_lake.jpeg"

19

},

20

{

21

title: "Miniques Lagoons",

22

src: "miniques_lagoon.jpg"

23

},

24

]

25

26

export default function Index() {

27

28

const [selectedProject, setSelectedProject] = useState(0);

29

const container = useRef(null);

30

const imageContainer = useRef(null);

31

32

useLayoutEffect( () => {

33

gsap.registerPlugin(ScrollTrigger);

34

ScrollTrigger.create({

35

trigger: imageContainer.current,

36

pin: true,

37

start: "top-=100px",

38

end: document.body.offsetHeight - window.innerHeight - 50,

39

})

40

}, [])

41

42

return (

43

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

44

<div className={styles.projectDescription}>

45

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

46

<Image

47

src={`/images/${projects[selectedProject].src}`}

48

fill={true}

49

alt="project image"

50

priority={true}

51

/>

52

</div>

53

<div className={styles.column}>

54

<p>The flora is characterized by the presence of high elevation wetland, as well as yellow straw, broom sedge, tola de agua and tola amaia.</p>

55

</div>

56

<div className={styles.column}>

57

<p>Some, like the southern viscacha, vicuña and Darwins rhea, are classified as endangered species. Others, such as Andean goose, horned coot, Andean gull, puna tinamou and the three flamingo species inhabiting in Chile (Andean flamingo, Chilean flamingo, and Jamess flamingo) are considered vulnerable.</p>

58

</div>

59

</div>

60

61

<div className={styles.projectList}>

62

{

63

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

64

return <div key={index} onMouseOver={() => {setSelectedProject(index)}} className={styles.projectEl}>

65

<h2>{project.title}</h2>

66

</div>

67

})

68

}

69

</div>

70

</div>

71

)

72

}

73

Notes about the code above:

  • The main image is pinned using a ScrollTrigger, that starts at -=100px of the top of the image and ends at 50px before the end of the scroll.
  • The state is managed with the mouse events when hovering a project, which dynamically changes the image.

Here's the result:

Wrapping up

We're offically done with this one-pager!

Hope you liked this tutorial, I've seen similar one-pager on a lot of awwwards winning website and so I thought it'd be interesting to know how it's possible to make something similar. 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