Hover Scrolling Gallery
Creating a hovering effect gallery using Framer and GSAP.
framer
gsap
react
css

DEMO
Effect we want to create is when user hovers over a link, in this case a project, you will be able to see a snap shot (a modal) of the project image and a link button to redirect to that page. As you hover over another project will scroll to that particular project giving it nice scrolling effect as you hover over the links.
All code will be in a single file as to not confuse people but feel free to divide and conquer.
I've first created main component file called HoverProjectGallery.jsx:
Effect we want to create is when user hovers over a link, in this case a project, you will be able to see a snap shot (a modal) of the project image and a link button to redirect to that page. As you hover over another project will scroll to that particular project giving it nice scrolling effect as you hover over the links.
All code will be in a single file as to not confuse people but feel free to divide and conquer.
I've first created main component file called HoverProjectGallery.jsx:
import { useEffect, useRef, useState } from "react"; import { motion } from "framer-motion"; import gsap from "gsap"; import styles from "./styles.module.scss"; const HoverProjectGallery = () => { const [modal, setModal] = useState({ active: false, index: 0 }); return ( <main> <div className={styles.container}> {projects.map((project, index) => { return ( <Project index={index} title={project.title} setModal={setModal} key={index} /> ); })} </div> <Modal modal={modal} projects={projects} /> </main> ); }; export default HoverProjectGallery;
The modal container will display the snap shot and the actual link button along with scrolling effect. Modal state has two properties passed in active and index to keep track of current index and current active project that is hovered over.
I've also created some projects in a array along with some images of the project.
import iGimdev from "./images/gimdev_thumb.png"; import iPokemon from "./images/pokemon_thumb.png"; import iFlyaway from "./images/flyaway_thumb.png"; import iOdinbook from "./images/odinbook_thumb.png"; const projects = [ { title: "GimDev", src: iGimdev, color: "#ffffff", }, { title: "Memory", src: iPokemon, color: "#ffffff", }, { title: "Flyaway", src: iFlyaway, color: "#ffffff", }, { title: "Odinbook", src: iOdinbook, color: "#ffffff", }, ];
Now lets create the Modal component as follow:
function Modal({ modal, projects }) { const { active, index } = modal; const modalContainer = useRef(null); const cursor = useRef(null); const cursorLabel = useRef(null); useEffect(() => { //Move Container let xMoveContainer = gsap.quickTo(modalContainer.current, "left", { duration: 0.8, ease: "power3", }); let yMoveContainer = gsap.quickTo(modalContainer.current, "top", { duration: 0.8, ease: "power3", }); //Move cursor let xMoveCursor = gsap.quickTo(cursor.current, "left", { duration: 0.5, ease: "power3", }); let yMoveCursor = gsap.quickTo(cursor.current, "top", { duration: 0.5, ease: "power3", }); //Move cursor label let xMoveCursorLabel = gsap.quickTo(cursorLabel.current, "left", { duration: 0.45, ease: "power3", }); let yMoveCursorLabel = gsap.quickTo(cursorLabel.current, "top", { duration: 0.45, ease: "power3", }); window.addEventListener("mousemove", (e) => { const { pageX, pageY } = e; xMoveContainer(pageX); yMoveContainer(pageY); xMoveCursor(pageX); yMoveCursor(pageY); xMoveCursorLabel(pageX); yMoveCursorLabel(pageY); }); }, []); return ( <> <motion.div ref={modalContainer} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"} className={styles.modalContainer} > <div style={{ top: index * -100 + "%" }} className={styles.modalSlider}> {projects.map((project, index) => { const { src, color } = project; return ( <div className={styles.modal} style={{ backgroundColor: color }} key={`modal_${index}`} > <img src={project.src} width={300} height={0} alt="image" /> </div> ); })} </div> </motion.div> <motion.div ref={cursor} className={styles.cursor} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"} ></motion.div> <motion.div ref={cursorLabel} className={styles.cursorLabel} variants={scaleAnimation} initial="initial" animate={active ? "enter" : "closed"} > View </motion.div> </> ); }
This is a quite long one so lets go through them.
The component receives two props modal state and projects data.
- modalContainer: Reference to the entire modal container
- cursor: A reference to the custom cursor element.
- cursorLabel: A reference to the label of the custom cursor (eg. "View").
Following this we have GSAP animations useEffect. This useEffect hook runs once when the component mounts, setting up GSAP animations for moving the modal, the cursor, and the cursor label in response to the mouse movements.
- gsap.quickTo: This creates animation function that move the elements (modalContainer, cursor & cursorLabel) to the specified left and top positions.
- window.addEventListener("mousemove" , ...) : Every time the mouse moves, the event provides pageX and pageY coordinates, which are used to update the positions of the three ref's the modal, the cursor and the label.
And finally we have motion.div from framer-motion for animating the entrance and exit. Initial and animate properties control the animation states based on weather modal.active is true or false.
The scaleAnimation specifies the animation transitions as follow:
const scaleAnimation = { initial: { scale: 0, x: "-50%", y: "-50%" }, enter: { scale: 1, x: "-50%", y: "-50%", transition: { duration: 0.4, ease: [0.76, 0, 0.24, 1] }, }, closed: { scale: 0, x: "-50%", y: "-50%", transition: { duration: 0.4, ease: [0.32, 0, 0.67, 0] }, }, };
The modal slider is a container for all the project elements. Each project inside the slider is mapped and rendered as a div, and each one is styled based on the project's color along with image within each modal item.
The custom cursor is represented by another motion.div with the class styles.cursor and the position is controlled by the mouse movement via GSAP. It is same with cursor label.
Quick note about gsap.quickTo which creates optimised tweening functions for smooth animations. Instead of specifiying animation details every time, it store the animation logic in variables like xMoveContainer, and then call those functions on mousemove to smoothly update element positions.
So putting it all together, the modal, cursor and label all move together based on mouse movement. GSAP is used to animate the elements smoothly and Framer Motion handles modal transitions(enter/exit animations).
Result provides visually interactive modal slider, along with a custom animated cursor and label that follow the user's mouse movement, all driven by GSAP for smooth animations and Framer Motion for modal transitions.
DEMO