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>
[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);
}
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",
});
}
});
});
});
});
<!-- 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>
| 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 |
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>
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>.