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 project gallery will be composed of 3 different components:
- Page Component: the parent, has the state and the data and imports the other two components.
- Project Component: a component that represents a single project.
- Modal Component: a component that represents the dynamic modal.
The Page component
The page component /app/page.js
is the parent of the project gallery and is responsable for the data and the state.
2
import styles from './page.module.css'
3
import { useState } from 'react';
4
import Project from '../components/project';
5
import Modal from '../components/modal';
10
src: "c2montreal.png",
14
title: "Office Studio",
15
src: "officestudio.png",
20
src: "locomotive.png",
30
export default function Home() {
32
const [modal, setModal] = useState({active: false, index: 0})
35
<main className={styles.main}>
36
<div className={styles.body}>
38
projects.map( (project, index) => {
39
return <Project index={index} title={project.title} setModal={setModal} key={index}/>
43
<Modal modal={modal} projects={projects}/>
Note: With that code, we should still see a blank page, because the Project
and Modal
components are still empty.
The Project Component
The project component represents a single project and has a hover interactivity on it. When hovering it, there's a small css animation and it also sets the index
inside of the modal state.
2
import React from 'react'
3
import styles from './style.module.css';
5
export default function index({index, title, setModal}) {
8
<div onMouseEnter={() => {setModal({active: true, index})}} onMouseLeave={() => {setModal({active: false, index})}} className={styles.project}>
10
<p>Design & Development</p>
- Line 8: We set the state of the modal when hovering the project.
- The project animations are done with pure CSS.
We should have something like this:
The Modal Component
Now we enter the nitty gritty, the modal is defintely the hard part of this tutorial. To do it, we will use the next/image
, the GSAP
library and Framer Motion
Let's install the required libraries:
npm i gsap framer-motion
Next let's start by doing the enter and exit animation of the modal. We basically use Framer Motion to scale it up and down. We'll also use CSS for the slide animation.
1
import { useRef } from 'react';
2
import { motion } from 'framer-motion';
3
import Image from 'next/image';
4
import styles from './style.module.css';
6
const scaleAnimation = {
7
initial: {scale: 0, x:"-50%", y:"-50%"},
8
enter: {scale: 1, x:"-50%", y:"-50%", transition: {duration: 0.4, ease: [0.76, 0, 0.24, 1]}},
9
closed: {scale: 0, x:"-50%", y:"-50%", transition: {duration: 0.4, ease: [0.32, 0, 0.67, 0]}}
12
export default function index({modal, projects}) {
14
const { active, index } = modal;
18
<motion.div variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"} className={styles.modalContainer}>
19
<div style={{top: index * -100 + "%"}} className={styles.modalSlider}>
21
projects.map( (project, index) => {
22
const { src, color } = project
23
return <div className={styles.modal} style={{backgroundColor: color}} key={`modal_${index}`}>
25
src={`/images/${src}`}
35
<motion.div className={styles.cursor} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"}></motion.div>
36
<motion.div className={styles.cursorLabel} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"}>View</motion.div>
Couple notes about the code:
- The scaling is made with Framer Motion through the
scaleAnimation
const. When a project is hovered, the state is changed which animates in and out the modal, the cursor and the cursor label. - The sliding animation is made through CSS, depending on the hovered project, the top position is adjusted accordingly.
Moving the modal along the mouse
To move the modal with the mouse move event, we use the GSAP
library to easily do that.
1
import { useRef, useEffect } from 'react';
2
import gsap from 'gsap';
5
const modalContainer = useRef(null);
6
const cursor = useRef(null);
7
const cursorLabel = useRef(null);
11
let xMoveContainer = gsap.quickTo(modalContainer.current, "left", {duration: 0.8, ease: "power3"})
12
let yMoveContainer = gsap.quickTo(modalContainer.current, "top", {duration: 0.8, ease: "power3"})
14
let xMoveCursor = gsap.quickTo(cursor.current, "left", {duration: 0.5, ease: "power3"})
15
let yMoveCursor = gsap.quickTo(cursor.current, "top", {duration: 0.5, ease: "power3"})
17
let xMoveCursorLabel = gsap.quickTo(cursorLabel.current, "left", {duration: 0.45, ease: "power3"})
18
let yMoveCursorLabel = gsap.quickTo(cursorLabel.current, "top", {duration: 0.45, ease: "power3"})
20
window.addEventListener('mousemove', (e) => {
21
const { pageX, pageY } = e;
26
xMoveCursorLabel(pageX)
27
yMoveCursorLabel(pageY)
33
<motion.div ref={modalContainer} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"} className={styles.modalContainer}>
34
<div style={{top: index * -100 + "%"}} className={styles.modalSlider}>
40
<motion.div ref={cursor} className={styles.cursor} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"}></motion.div>
41
<motion.div ref={cursorLabel} className={styles.cursorLabel} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"}>View</motion.div>
Couple notes about the code:
- 3 refs are created in order to target the modal, the cursor and the cursor label
- the movement is created with the
quickTo
function from GSAP
library. - Note that the durations vary in order to create a delay between the different moving elements.
Wrapping up
Hope you liked this tutorial, I've seen similar project galleries in a lot of awwwards winning website and so I thought it'd be intersting to know how it's possible to make something similar. Hope you learned something :)
-Oli