Make animated landing page in React

Om
7 min readFeb 6, 2023

--

Boilerplate setup

  1. yarn create vite
yarn create vite

2. Install tailwind

yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

> configure your template paths, Add the paths to all of your template files in your tailwind.config.cjs file.

content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],

> Add the Tailwind directives to your CSS

Add the @tailwind directives for each of Tailwind’s layers to your ./src/index.css file.

@tailwind base;
@tailwind components;
@tailwind utilities;

> For instance

<h1 className="text-3xl font-bold underline">
Hello world!
</h1>

> Update colors

/** @type {import('tailwindcss').Config} */

module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
coffee: "#E1DFD8",
"verge-black": "#131313",
},
},
},
plugins: [],
}
<div className="bg-coffee text-verge-black">
</div>

components/Header

const Header = () => {
return (
<header className="w-full text-2xl flex bg-clip-padding backdrop-filter backdrop-blur-xl bg-opacity-60 items-center justify-between p-6 bg-coffee fixed top-0 right-0">
<a href="/">Hugo</a>
<a href="">Contact</a>
</header>
)
}

export default Header

components/HeroIntroTitle

> install clsx,motion

yarn add motion,clsx
import clsx from "clsx"
import { animate, inView, stagger } from "motion"
import { useEffect } from "react"

type AnimatedLetterProps = {
letter: string
alignment: "left" | "right"
}
const AnimatedLetter = ({
letter,
alignment = "left",
}: AnimatedLetterProps) => {
return (
<span
className={clsx(
"inline-block text-[12vw] leading-[12vw] opacity-0 uppercase font-semibold",
{
"character-stagger-animation-left": alignment === "left",
"character-stagger-animation-right": alignment === "right",
}
)}
>
{letter}
</span>
)
}
const HeroMain = () => {
useEffect(() => {
inView(".character-stagger-animation-container", () => {
animate(
".character-stagger-animation-left",
{
transform: "none",
opacity: 1,
},
{
delay: stagger(0.1, {}),
duration: 2,
}
)
})
inView(".character-stagger-animation-container", () => {
animate(
".character-stagger-animation-right",
{
transform: "none",
opacity: 1,
},
{
delay: stagger(0.1, {}),
duration: 2,
}
)
})
return () => {}
}, [])
return (
<div className="p-10 h-screen flex items-center justify-center relative">
{/* Intro title */}
<div className="flex flex-col py-24 ">
<div className="mx-auto flex flex-col ">
<span className="inline-block character-stagger-animation-container leading-[12vw]">
<AnimatedLetter alignment="left" letter="C" />
<AnimatedLetter alignment="left" letter="r" />
<AnimatedLetter alignment="left" letter="e" />
<AnimatedLetter alignment="left" letter="a" />
<AnimatedLetter alignment="left" letter="t" />
<AnimatedLetter alignment="left" letter="i" />
<AnimatedLetter alignment="left" letter="v" />
<AnimatedLetter alignment="left" letter="e" />
</span>
<span className="inline-block character-stagger-animation-container leading-[12vw]">
<AnimatedLetter letter="D" alignment="right" />
<AnimatedLetter letter="e" alignment="right" />
<AnimatedLetter letter="v" alignment="right" />
<AnimatedLetter letter="e" alignment="right" />
<AnimatedLetter letter="l" alignment="right" />
<AnimatedLetter letter="o" alignment="right" />
<AnimatedLetter letter="p" alignment="right" />
<AnimatedLetter letter="e" alignment="right" />
<AnimatedLetter letter="r" alignment="right" />
</span>
</div>
</div>
</div>
)
}
export default HeroMain

Update css

.character-stagger-animation-left {
transform: translateX(10vw) rotateX(90deg);
}
.character-stagger-animation-right {
transform: translateX(-10vw) rotateX(-90deg);
}

components/About

import clsx from "clsx"
import { inView, animate } from "motion"
import { useEffect, useRef } from "react"

type AnimatedLineProps = {
line: string
}
const AnimatedLine = ({ line }: AnimatedLineProps) => {
const lineContainerRef = useRef<HTMLSpanElement>(null)
const lineRef = useRef<HTMLSpanElement>(null)
useEffect(() => {
if (!lineContainerRef.current) return
inView(lineContainerRef.current, () => {
if (!lineRef.current) return
animate(
lineRef.current,
{
transform: "none",
opacity: 1,
},
{
duration: 2,
}
)
})
return () => {}
}, [])
return (
<span
className="overflow-hidden text-3xl inline-block"
ref={lineContainerRef}
>
<span
className={clsx("text-3xl inline-block opacity-0 translate-y-[40px]")}
ref={lineRef}
>
{line}
</span>
</span>
)
}
const About = () => {
return (
<div className="h-screen mx-40 flex items-center justify-center ">
<div className=" text-center">
<div className="flex flex-col items-center justify-between">
<div className="">
<AnimatedLine line="I AM HUGO, CREATIVE DEVELOPER BASED IN FRANCE. I WORK AS A" />
<AnimatedLine line="FREELANCER WITH AGENCIES, START-UPS AND INDIVIDUALS. I HAVE A" />
<AnimatedLine line="FONDNESS FOR CLEAN DESIGNS, BEAUTIFUL TYPOGRAPHIES AND" />
<AnimatedLine line=" INTERACTIVE DEVELOPMENT." />
</div>
</div>
</div>
</div>
)
}
export default About

components/services

import { inView, animate } from "motion"
import { useEffect } from "react"

const TransformYAxisText = ({ title }: { title: string }) => {
useEffect(() => {
inView(".service-title-container", () => {
animate(
".service-title",
{
transform: "none",
opacity: 1,
},
{
duration: 2,
delay: 0.02,
}
)
})
return () => {}
}, [])
return (
<span className="inline-block service-title-container leading-[4vw] overflow-hidden">
<span className="service-title opacity-0 translate-y-[4vw] inline-block text-[4vw] leading-[4vw] uppercase font-semibold">
{title}
</span>
</span>
)
}
const Services = () => {
return (
<div className=" flex flex-col h-screen items-center justify-center">
<div className="mx-auto">
<div className="text-3xl">Services I can help you with</div>
<div className="h-10"></div>
<div className="flex flex-col">
<TransformYAxisText title="art direction. branding." />
<TransformYAxisText title=" iconography. illustration." />
<TransformYAxisText title="logo design. motion" />
<TransformYAxisText title=" ui. ux. websites." />
</div>
</div>
</div>
)
}
export default Services

components/selected-work

import { inView, animate, scroll } from "motion"
import { useEffect, useRef } from "react"

const SelectedWork = () => {
const selectedWorkContainerRef = useRef<HTMLDivElement>(null)
const selectedWorkRef = useRef<HTMLHeadingElement>(null)
useEffect(() => {
if (!selectedWorkContainerRef.current) return
inView(selectedWorkContainerRef.current, () => {
if (!selectedWorkRef.current) return
scroll(
animate(
selectedWorkRef.current,
{ x: [-200, 200] },
{
duration: 1.4,
}
),
{
target: selectedWorkRef.current,
}
)
})
}, [])
return (
<div
className="h-screen overflow-hidden flex items-center justify-center "
ref={selectedWorkContainerRef}
>
<h2 className="text-[12vw] uppercase" ref={selectedWorkRef}>
Selected work
</h2>
</div>
)
}
export default SelectedWork

components/work

  1. Copy data in constants/index.ts
export const PROJECTS = [
{
title: "Coca cola x marshmallow",
image: "https://images5.alphacoders.com/659/659089.jpg",
url: "",
prefix: "first",
},
{
title: "Decor",
image:
"https://images.prismic.io/andy-2022/ecc90d70-51ea-4b63-9649-b8be0c1325a0_scavolini.jpg?auto=compress,format&rect=0,0,2400,1440&w=1920&h=1152",
url: "",
prefix: "second",
},
{
title: "Tuscany",
image: "https://images3.alphacoders.com/127/1276158.jpg",
url: "",
prefix: "third",
},
{
title: "daily experiments",
image:
"https://cdn.shopify.com/s/files/1/0167/4484/products/white-1_1480x1836_crop_center.jpg?v=1595952757",
url: "",
prefix: "fourth",
},
{
title: "Raws",
image:
"https://images.prismic.io/andy-2022/7be715a9-6bbf-47f6-99e7-e0c672d98d6d_raws.jpg?auto=compress,format&rect=0,0,2400,1440&w=1920&h=1152",
url: "",
prefix: "fifth",
},
{
title: "Samsung",
image:
"https://duet-cdn.vox-cdn.com/thumbor/0x0:2040x1360/2048x1365/filters:focal(929x614:930x615):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24058703/DSCF9357_2.jpg",
url: "",
prefix: "sixth",
},
]

2. Update project

import clsx from "clsx"
import { animate } from "motion"
import { useState } from "react"
import { PROJECTS } from "../../constants"

interface IProject {
title: string
image: string
url: string
prefix: string
}
type ProjectProps = {
project: IProject
index: number
}
const Project = ({ project, index }: ProjectProps) => {
const { image, title, url, prefix } = project
const [activeImage, setActiveImage] = useState(-1)
const handleOnMouseEnter = () => {
setActiveImage(index)
const activeImage = document.querySelector("." + prefix + "-image")
if (!activeImage) return
animate(
activeImage,
{
opacity: 1,
transform: "scale(1.2)",
},
{
duration: 1.4,
}
)
}
const handleMouseLeave = () => {
setActiveImage(-1)
const activeImage = document.querySelector("." + prefix + "-image")
if (!activeImage) return
animate(activeImage, {
opacity: 0,
transform: "none",
})
}
return (
<div
className={clsx("flex border-b-2 border-black py-24", {})}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div
className={clsx(
prefix +
"-image-wrapper flex items-center justify-between flex-1 mr-20 "
)}
>
<h2 className={clsx("text-[4vw] uppercase", {})}>{title}</h2>
<div className="overflow-hidden w-[420px] h-[256px] rounded-2xl">
<img
className={clsx(prefix + "-image", " rounded-2xl opacity-0", {
"opacity-100": activeImage === index,
})}
width={420}
height={256}
src={image}
alt=""
/>
</div>
</div>
</div>
)
}
const Work = () => {
return (
<div className="mx-auto px-24 py-40 ">
<div className="overflow-hidden">
{PROJECTS.map((project, index) => (
<Project key={project.title} project={project} index={index} />
))}
</div>
</div>
)
}
export default Work

components/contact

type LinkProps = {
url: string
}

const Link = ({ url }: LinkProps) => {
return (
<span className="text-2xl inline-block cursor-pointer uppercase link-hover">
{url}
</span>
)
}
const Contact = () => {
return (
<div className="flex flex-col h-screen items-center justify-center ">
<div className="mx-auto">
<div className="text-3xl text-bright font-semibold">
I am available now to work on project
</div>
<div className="h-10"></div>
<h2 className="text-6xl leading-snug uppercase font-medium cursor-pointer link-hover">
Send me an email
</h2>
<div className="h-40"></div>
<span className="flex flex-row items-start space-x-10">
<Link url="dribble" />
<Link url="instagram" />
<Link url="behance" />
<Link url="linkedin" />
</span>
</div>
</div>
)
}
export default Contact

styles

/* link hover animation  */

.link-hover {
position: relative;
color: black;
transition: color 0.95s cubic-bezier(0.19, 1, 0.22, 1);
}
.link-hover:before {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0.075em;
min-height: 1px;
transform: scaleX(1);
transform-origin: left;
background-color: currentColor;
transition: transform 0.95s cubic-bezier(0.19, 1, 0.22, 1);
}
.link-hover:not(:hover) {
color: black;
}
.link-hover:not(:hover):before {
transform-origin: right;
transform: scaleX(0);
}

Smooth scrolling

yarn add @studio-freight/lenis
useEffect(() => {
const lenis = new Lenis({
duration: 1.6,
})
function raf(time: number) {
lenis.raf(time)
requestAnimationFrame(raf)
}
requestAnimationFrame(raf)
}, [])

> styles

html {
scroll-behavior: initial !important;
}
html,
body {
min-height: 100%;
height: auto;
}

Hide scrollbar

/* hide scrollbar */
*::-webkit-scrollbar {
width: 0px;
}
*::-webkit-scrollbar-track {
background: #e0b3d1;
}
*::-webkit-scrollbar-thumb {
background-color: #450adf; border-radius: 4px;
border: 0px solid #e0b3d1;
}

--

--