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.
This landing page will be composed of 4 components:
clip-path animation and a body with an animated title and image.The 3 sub-components are created inside a /src/components/ folder.
The page component is the parent of the other 3 components. It also manages the smooth scroll, which is easily done with Locomotive Scroll v5.
page.js"use client";
import { useEffect } from "react";
import styles from "./page.module.css";
import Intro from "../components/Intro";
import Description from "../components/Description";
import Projects from "../components/Projects";
export default function Home() {
useEffect(() => {
(async () => {
const LocomotiveScroll = (await import("locomotive-scroll")).default;
const locomotiveScroll = new LocomotiveScroll();
})();
}, []);
return (
<main className={styles.main}>
<Intro />
<Description />
<Projects />
</main>
);
}Note: The Locomotive Scroll is purely a client-side library, so we need to import it asynchronously after the component mounts. If we don't do this, we will receive an error mentioning that
windowdoes not exist.
With that, we now have a native smooth scroll, amazing!
The Intro is composed of 3 main elements:
clip-path property.height property and Locomotive Scroll with data-scroll-speed.data-scroll-speed.Intro/index.jsx"use client";
import React, { useLayoutEffect, useRef } from "react";
import styles from "./style.module.css";
import Image from "next/image";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
export default function Index() {
const background = useRef(null);
const introImage = useRef(null);
const homeHeader = useRef(null);
useLayoutEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const timeline = gsap.timeline({
scrollTrigger: {
trigger: document.documentElement,
scrub: true,
start: "top",
end: "+=500px",
},
});
timeline
.from(background.current, { clipPath: `inset(15%)` })
.to(introImage.current, { height: "200px" }, 0);
}, []);
return (
<div ref={homeHeader} className={styles.homeHeader}>
<div className={styles.backgroundImage} ref={background}>
<Image
src={"/images/background.jpeg"}
fill={true}
alt="background image"
priority={true}
/>
</div>
<div className={styles.intro}>
<div
ref={introImage}
data-scroll
data-scroll-speed="0.3"
className={styles.introImage}
>
<Image
src={"/images/intro.png"}
alt="intro image"
fill={true}
priority={true}
/>
</div>
<h1 data-scroll data-scroll-speed="0.7">
SMOOTH SCROLL
</h1>
</div>
</div>
);
}The Description component features an internal component called AnimatedText. An array of phrases is mapped to render each line.
Inside it, a new ScrollTrigger is created from the top of each paragraph and ends at +=400px. The left offset value and the opacity are adjusted during the scroll.
Description/index.jsximport React, { useLayoutEffect, useRef } from "react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import gsap from "gsap";
import styles from "./style.module.css";
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)",
];
export default function Index() {
return (
<div className={styles.description}>
{phrases.map((phrase, index) => {
return <AnimatedText key={index}>{phrase}</AnimatedText>;
})}
</div>
);
}
function AnimatedText({ children }) {
const text = useRef(null);
useLayoutEffect(() => {
gsap.registerPlugin(ScrollTrigger);
gsap.from(text.current, {
scrollTrigger: {
trigger: text.current,
scrub: true,
start: "0px bottom",
end: "bottom+=400px bottom",
},
opacity: 0,
left: "-200px",
ease: "power3.Out",
});
}, []);
return <p ref={text}>{children}</p>;
}The Projects component features standard layout styling with a pinned sidebar animation.
There is an internal state to track which project is currently highlighted via hover, which dynamically updates the source URL of the showcased image.
Projects/index.jsximport React, { useState, useLayoutEffect, useRef } from "react";
import styles from "./style.module.css";
import Image from "next/image";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const projects = [
{
title: "Salar de Atacama",
src: "salar_de_atacama.jpg",
},
{
title: "Valle de la luna",
src: "valle_de_la_muerte.jpeg",
},
{
title: "Miscanti Lake",
src: "miscani_lake.jpeg",
},
{
title: "Miniques Lagoons",
src: "miniques_lagoon.jpg",
},
];
export default function Index() {
const [selectedProject, setSelectedProject] = useState(0);
const container = useRef(null);
const imageContainer = useRef(null);
useLayoutEffect(() => {
gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.create({
trigger: imageContainer.current,
pin: true,
start: "top-=100px",
end: document.body.offsetHeight - window.innerHeight - 50,
});
}, []);
return (
<div ref={container} className={styles.projects}>
<div className={styles.projectDescription}>
<div ref={imageContainer} className={styles.imageContainer}>
<Image
src={`/images/${projects[selectedProject].src}`}
fill={true}
alt="project image"
priority={true}
/>
</div>
<div className={styles.column}>
<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>
</div>
<div className={styles.column}>
<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>
</div>
</div>
<div className={styles.projectList}>
{projects.map((project, index) => {
return (
<div
key={index}
onMouseOver={() => {
setSelectedProject(index);
}}
className={styles.projectEl}
>
<h2>{project.title}</h2>
</div>
);
})}
</div>
</div>
);
}We're officially done with this one-pager!
Hope you liked this tutorial. I've seen similar layouts on many award-winning portfolios, so it's a great technique to learn.