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
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';
8
export default function Home() {
13
const LocomotiveScroll = (await import('locomotive-scroll')).default
14
const locomotiveScroll = new LocomotiveScroll();
20
<main className={styles.main}>
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.
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
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';
8
export default function Index() {
10
const background = useRef(null);
11
const introImage = useRef(null);
12
const homeHeader = useRef(null);
14
useLayoutEffect( () => {
15
gsap.registerPlugin(ScrollTrigger);
17
const timeline = gsap.timeline({
19
trigger: document.documentElement,
27
.from(background.current, {clipPath: `inset(15%)`})
28
.to(introImage.current, {height: "200px"}, 0)
32
<div ref={homeHeader} className={styles.homeHeader}>
33
<div className={styles.backgroundImage} ref={background}>
35
src={'/images/background.jpeg'}
37
alt="background image"
41
<div className={styles.intro}>
42
<div ref={introImage} data-scroll data-scroll-speed="0.3" className={styles.introImage}>
44
src={'/images/intro.png'}
50
<h1 data-scroll data-scroll-speed="0.7">SMOOTH SCROLL</h1>
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.
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';
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)"]
8
export default function Index() {
11
<div className={styles.description} >
13
phrases.map( (phrase, index) => {
14
return <AnimatedText key={index}>{phrase}</AnimatedText>
21
function AnimatedText({children}) {
22
const text = useRef(null);
24
useLayoutEffect( () => {
25
gsap.registerPlugin(ScrollTrigger);
26
gsap.from(text.current, {
28
trigger: text.current,
31
end: "bottom+=400px bottom",
39
return <p ref={text}>{children}</p>
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.
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';
9
title: "Salar de Atacama",
10
src: "salar_de_atacama.jpg"
13
title: "Valle de la luna",
14
src: "valle_de_la_muerte.jpeg"
17
title: "Miscanti Lake",
18
src: "miscani_lake.jpeg"
21
title: "Miniques Lagoons",
22
src: "miniques_lagoon.jpg"
26
export default function Index() {
28
const [selectedProject, setSelectedProject] = useState(0);
29
const container = useRef(null);
30
const imageContainer = useRef(null);
32
useLayoutEffect( () => {
33
gsap.registerPlugin(ScrollTrigger);
34
ScrollTrigger.create({
35
trigger: imageContainer.current,
38
end: document.body.offsetHeight - window.innerHeight - 50,
43
<div ref={container} className={styles.projects}>
44
<div className={styles.projectDescription}>
45
<div ref={imageContainer} className={styles.imageContainer}>
47
src={`/images/${projects[selectedProject].src}`}
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>
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>
61
<div className={styles.projectList}>
63
projects.map( (project, index) => {
64
return <div key={index} onMouseOver={() => {setSelectedProject(index)}} className={styles.projectEl}>
65
<h2>{project.title}</h2>
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