Unveiling Tomorrow's Innovations, Transforming Today's Dreams
Add these to your <head> or before closing </body> tag:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/Flip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/CustomEase.min.js"></script>
[data-flip-gallery] {
width: 100vw;
position: relative;
}
[data-flip-container] {
width: 100%;
padding-top: 100vh;
}
[data-flip-item] {
position: relative;
margin-bottom: 1em;
width: 400px;
height: 500px;
overflow: hidden;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
[data-flip-item]:nth-child(odd) {
left: 75%;
}
[data-flip-item]:nth-child(even) {
left: 25%;
}
[data-flip-item].is-stacked {
position: absolute;
top: 47.5%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 400px;
}
[data-flip-trigger] {
position: absolute;
top: 75%;
left: 50%;
transform: translate(-50%, -50%);
background: #0084ff;
color: #fff;
font-family: inherit;
font-size: 12px;
border-radius: 30px;
padding: 1em 2em;
cursor: pointer;
z-index: 2;
border: none;
}
<!-- Gallery wrapper with config -->
<div data-flip-gallery data-flip-duration="2" data-flip-stagger="0.05">
<div data-flip-container class="is-stacked">
<!-- Each item: rotation value controls the stacked fan angle -->
<div data-flip-item class="is-stacked" data-flip-rotation="10">
<img src="image1.jpg" />
</div>
<div data-flip-item class="is-stacked" data-flip-rotation="-5">
<img src="image2.jpg" />
</div>
<div data-flip-item class="is-stacked" data-flip-rotation="2">
<img src="image3.jpg" />
</div>
<div data-flip-item class="is-stacked" data-flip-rotation="-2">
<img src="image4.jpg" />
</div>
</div>
</div>
<!-- Trigger button -->
<button
data-flip-trigger
data-flip-text-expand="Explore Ideas"
data-flip-text-collapse="Hide All Ideas"
>
Explore Ideas
</button>
gsap.registerPlugin(Flip);
CustomEase.create("cubic", "0.83, 0, 0.17, 1");
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("[data-flip-gallery]").forEach((gallery) => {
const container = gallery.querySelector("[data-flip-container]");
const items = gsap.utils.toArray(
gallery.querySelectorAll("[data-flip-item]")
);
const duration = parseFloat(gallery.dataset.flipDuration) || 2;
const stagger = parseFloat(gallery.dataset.flipStagger) || 0.05;
const trigger = document.querySelector("[data-flip-trigger]");
const expandText = trigger?.dataset.flipTextExpand || "Explore Ideas";
const collapseText = trigger?.dataset.flipTextCollapse || "Hide All Ideas";
let isStacked = container.classList.contains("is-stacked");
// Apply initial rotation from data attributes
if (isStacked) {
items.forEach((item) => {
const rotation = parseFloat(item.dataset.flipRotation) || 0;
gsap.set(item, { rotate: rotation });
});
}
function applyRotation() {
items.forEach((item) => {
const rotation = isStacked
? parseFloat(item.dataset.flipRotation) || 0
: 0;
gsap.to(item, {
rotate: rotation,
duration: duration,
ease: "cubic",
delay: 0.15,
});
});
}
trigger?.addEventListener("click", () => {
isStacked = !isStacked;
setTimeout(() => {
trigger.textContent = isStacked ? expandText : collapseText;
}, 1000);
const state = Flip.getState(
"[data-flip-container], [data-flip-item]"
);
container.classList.toggle("is-stacked");
items.forEach((item) => item.classList.toggle("is-stacked"));
Flip.from(state, {
absolute: true,
duration: duration,
rotate: 0,
stagger: stagger,
ease: "cubic",
onStart: () => applyRotation(),
});
});
});
});
| Attribute | On | Default | Description |
|---|---|---|---|
data-flip-gallery |
Gallery wrapper | required | Enables the flip gallery on this element |
data-flip-container |
Inner container | required | Direct child that holds all items |
data-flip-item |
Each gallery item | required | Marks an element as a flip-animated item |
data-flip-trigger |
Button | required | Clicking this toggles stacked/spread view |
data-flip-duration |
Gallery wrapper | 2 |
Animation duration in seconds |
data-flip-stagger |
Gallery wrapper | 0.05 |
Delay between each item's animation |
data-flip-rotation |
Each item | 0 |
Rotation angle (degrees) in stacked view |
data-flip-text-expand |
Trigger button | Explore Ideas |
Button text when gallery is stacked |
data-flip-text-collapse |
Trigger button | Hide All Ideas |
Button text when gallery is spread |
The animation relies on two CSS states controlled by the
is-stacked class:
Stacked state (initial) — items are
position: absolute, centered and overlapping with
individual rotation values creating a fanned card pile effect.
Spread state (toggled) — items are
position: relative, laid out vertically with alternating
left/right positions using nth-child.
GSAP Flip captures the element positions before the class toggle, then
animates from the old positions to the new ones. The
CustomEase with cubic values
(0.83, 0, 0.17, 1) creates a smooth, snappy transition.
<!-- Faster animation with more stagger -->
<div data-flip-gallery data-flip-duration="1.2" data-flip-stagger="0.12">
...
</div>
<!-- Dramatic rotation values -->
<div data-flip-item class="is-stacked" data-flip-rotation="25">...</div>
<div data-flip-item class="is-stacked" data-flip-rotation="-15">...</div>
<!-- Custom button labels -->
<button
data-flip-trigger
data-flip-text-expand="Show Gallery"
data-flip-text-collapse="Close Gallery"
>
Show Gallery
</button>