Demo 01

Horizontal Expand Gallery

Demo 02

Fast Expand · 3 Panels

Demo 03

Elastic Ease · Rounded Panels

Demo 04

Slow & Dramatic

1. CDN Script

Add this to your <head> or before closing </body> tag:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>

2. Required CSS

[data-expand-hover] {
  height: 100%;
  width: 100%;
}

.expand-gallery {
  display: flex;
  height: 100%;
  width: 100%;
  gap: var(--expand-gap, 6px);
  overflow: hidden;
}

.expand-panel {
  position: relative;
  flex: var(--expand-collapsed, 1);
  overflow: hidden;
  border-radius: var(--expand-radius, 12px);
  cursor: pointer;
}

.expand-panel img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.expand-panel .panel-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.75) 0%,
    rgba(0, 0, 0, 0) 50%
  );
  pointer-events: none;
  z-index: 1;
}

.expand-panel .panel-content {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 1.5rem;
  z-index: 2;
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
}

.expand-panel .panel-content h3 {
  font-size: 1.25rem;
  font-weight: 700;
  margin-bottom: 0.25rem;
  color: #fff;
}

.expand-panel .panel-content p {
  font-size: 0.875rem;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.7);
}

3. JavaScript

document.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll("[data-expand-hover]").forEach((container) => {
    const expandFlex = parseFloat(container.dataset.expandFlex) || 4;
    const collapsedFlex = parseFloat(container.dataset.expandCollapsed) || 1;
    const duration = parseFloat(container.dataset.expandDuration) || 0.5;
    const ease = container.dataset.expandEase || "power3.out";
    const gap = parseFloat(container.dataset.expandGap) || 6;
    const radius = parseFloat(container.dataset.expandRadius) || 12;

    // Build the gallery wrapper
    const gallery = document.createElement("div");
    gallery.className = "expand-gallery";
    gallery.style.setProperty("--expand-gap", gap + "px");
    gallery.style.setProperty("--expand-radius", radius + "px");
    gallery.style.setProperty("--expand-collapsed", collapsedFlex);

    // Move panels into the gallery
    const panels = Array.from(container.querySelectorAll(".expand-panel"));
    panels.forEach((panel) => gallery.appendChild(panel));
    container.appendChild(gallery);

    // Set initial border-radius on panels
    panels.forEach((panel) => {
      panel.style.borderRadius = radius + "px";
    });

    // Hover handlers
    panels.forEach((panel) => {
      const content = panel.querySelector(".panel-content");

      panel.addEventListener("mouseenter", () => {
        // Expand hovered panel
        gsap.to(panel, { flex: expandFlex, duration, ease });
        // Fade in content
        if (content) {
          gsap.to(content, {
            opacity: 1,
            y: 0,
            duration: duration * 0.6,
            delay: duration * 0.3,
            ease: "power2.out",
          });
        }
        // Collapse siblings
        panels.forEach((sibling) => {
          if (sibling !== panel) {
            gsap.to(sibling, { flex: collapsedFlex, duration, ease });
            const sibContent = sibling.querySelector(".panel-content");
            if (sibContent) {
              gsap.to(sibContent, {
                opacity: 0,
                y: 10,
                duration: duration * 0.3,
                ease: "power2.in",
              });
            }
          }
        });
      });

      panel.addEventListener("mouseleave", () => {
        // Check if mouse is still inside gallery
        // Reset handled at gallery level
      });
    });

    // Reset all panels when leaving the gallery
    gallery.addEventListener("mouseleave", () => {
      panels.forEach((panel) => {
        gsap.to(panel, { flex: collapsedFlex, duration, ease });
        const content = panel.querySelector(".panel-content");
        if (content) {
          gsap.to(content, {
            opacity: 0,
            y: 10,
            duration: duration * 0.3,
            ease: "power2.in",
          });
        }
      });
    });
  });
});

4. HTML Usage

<!-- Default expand gallery -->
<div data-expand-hover>
  <div class="expand-panel">
    <img src="your-image.jpg" alt="Description" />
    <div class="panel-overlay"></div>
    <div class="panel-content">
      <h3>Title</h3>
      <p>Subtitle text</p>
    </div>
  </div>
  <div class="expand-panel">
    <img src="your-image-2.jpg" alt="Description" />
    <div class="panel-overlay"></div>
    <div class="panel-content">
      <h3>Title</h3>
      <p>Subtitle text</p>
    </div>
  </div>
  <!-- ...more panels -->
</div>

<!-- Custom settings -->
<div
  data-expand-hover
  data-expand-flex="5"
  data-expand-duration="0.35"
  data-expand-ease="power2.out"
  data-expand-gap="8"
  data-expand-radius="16"
  data-expand-collapsed="0.6"
>
  <!-- panels here -->
</div>

5. Data Attributes Reference

Attribute Default Description
data-expand-hover required Enables the expand animation on the container
data-expand-flex 4 Flex value of the expanded panel
data-expand-collapsed 1 Flex value of collapsed panels
data-expand-duration 0.5 Animation duration in seconds
data-expand-ease power3.out GSAP easing function
data-expand-gap 6 Gap between panels in pixels
data-expand-radius 12 Border radius in pixels

6. Panel Structure

Each .expand-panel can contain three optional layers:

<div class="expand-panel">
  <!-- 1. Background image (required) -->
  <img src="..." alt="..." />

  <!-- 2. Gradient overlay (optional) -->
  <div class="panel-overlay"></div>

  <!-- 3. Text content - fades in on expand (optional) -->
  <div class="panel-content">
    <h3>Title</h3>
    <p>Description</p>
  </div>
</div>

7. WebStudio Note

In WebStudio, wrap your panels inside a div element and add data-expand-hover plus any config attributes to that wrapper. Place each .expand-panel as a direct child. Set the parent container height explicitly (e.g. 500px) since the gallery fills 100% of its parent. Paste the CSS into your project styles and the JS into a custom code embed before </body>.