GSAP - Simple Example

by Hugo Vale Pereira

HTML

<!--Tutorial from: https://www.youtube.com/watch?v=Gy9IS0Y6dy0  Nov 2025
inspired from: https://www.wildyriftian.com/works
-->

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
<nav>
  <p>Tutorial</p>
  <p>GSAP</p>
</nav>
<div class="folders">
  <div class="row">
    <div class="folder variant-1">
      <div class="folder-preview">
        <div class="folder-preview-img">
          <img
            src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
          />
        </div>
        <div class="folder-preview-img">
          <img
            src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
          />
        </div>
        <div class="folder-preview-img">
          <img
            src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
          />
        </div>
      </div>
      <div class="folder-wrapper">
        <div class="folder-index">
          <p>01</p>
        </div>
        <div class="folder-name"><h1>figures</h1></div>
      </div>
    </div>
    <div class="folder variant-2">
      <div class="folder-preview">
        <div class="folder-preview-img">
          <img
            src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
          />
        </div>
        <div class="folder-preview-img">
          <img
            src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"
          />
        </div>
        <div class="folder-preview-img">
          <img
           ...

CSS

@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap');

:root {
  --bg: #dddddd;
  --fg: #1d1d1d;
  --variant-1: oklch(0.9412 0.2245 111.18);
  --variant-2: oklch(0.6647 0.2695 31.76);
  --variant-3: #bdbdbd;
  --disabled-folder-bg: #d2d2d2;
  --disabled-folder-fg: #8a8a8a;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background: var(--bg);
  color: var(--fg);
}

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

h1{
  font-family: "DM Sans" ,-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
  font-size: 2.75rem;
  font-weight: 400;

}

p{
  text-transform: uppercase;
  font-family: "DM Mono";
  font-size: 0.8rem;
  font-weight: 500;
}



h1,
p {
  transition: color 250ms ease;
}

nav {
  position: absolute;
  top: 0;
  width: 100%;
  padding: 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.folders {
  width: 100%;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
}

.row {
  position: relative;
  width: 100%;
  display: flex;
}

.folder {
  position: relative;
  flex: 1;
  height: 200px;
  display: flex;
  flex-direction: column;
  cursor: pointer;
}

.row:nth-child(2) .folder:nth-child(1) {
  flex: 2;
}

.row:nth-child(2) .folder:nth-child(2) {
  flex: 3;
}

.folder-preview {
  position: absolute;
  top: 0;
  left: 0;
  width: 25rem;
  height: 100%;
  pointer-events: none;
}

.folder-preview-img {
  position: absolute;
  top: 50%;
  width: 8rem;
  height: 12rem;
}

.folder-preview-img:nth-child(1) {
  left: 20%;
  transform-origin: top left;
}

.folder-preview-img:nth-child(2) {
  left: 50%;
  transform-origin:...

JavaScript

const folders = document.querySelectorAll(".folder");
const folderWrappers = document.querySelectorAll(".folder-wrapper");

let isMobile = window.innerWidth < 678;

function setInitialPositions() {
  gsap.set(folderWrappers, { y: isMobile ? 0 : 25 });
}

folders.forEach((folder, index) => {
  const previewImages = folder.querySelectorAll(".folder-preview-img");
  folder.addEventListener("mouseenter", () => {
    if (isMobile) return;
    folders.forEach((siblingFolder) => {
      if (siblingFolder !== folder) {
        siblingFolder.classList.add("disabled");
      }
    });

    gsap.to(folderWrappers[index], {
      y: 0,
      duration: 0.25,
      ease: "back.out(1.7)",
    });

    previewImages.forEach((img, imgIndex) => {
      let rotation;
      if (imgIndex === 0) {
        rotation = gsap.utils.random(-20, -10);
      } else if (imgIndex === 1) {
        rotation = gsap.utils.random(-10, 10);
      } else if (imgIndex === 2) {
        rotation = gsap.utils.random(10, 20);
      }

      gsap.to(img, {
        y: "-100%",
        rotation: rotation,
        duration: 0.25,
        ease: "back.out(1.7)",
        delay: imgIndex * 0.025,
      });
    });
  });

  folder.addEventListener("mouseleave", () => {
    if (isMobile) return;

    folders.forEach((siblingFolder) => {
      siblingFolder.classList.remove("disabled");
    });

    gsap.to(folderWrappers[index], {
      y: 25,
      duration: 0.25,
      ease: "back.out(1.7)",
    });

    previewImages.
      forEach((img, imgIndex) => {
        gsap.to(img, {
          y: "0%",
          rotation: 0,
          duration: 0.25,
          ease: "back.out(1.7)",
          delay: imgIndex * 0.05,
        });
      });
  });
});

window.addEventListener('resize', ()=> {
  const currentBreakpoint = window.innerWidth < 678;
  if (currentBreakpoint !== isMobile){
    isMobile = currentBreakpoint;
    setInitialPositions();

   ...