Layout Grid Flip
Toggle between large and small grid layouts with a smooth GSAP Flip animation. Cards reposition from their old layout to the new one in a single coordinated move, with the container height tweening in sync.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script><script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Flip.min.js"></script>Code
<div data-layout-status="large" data-layout-group class="layout-group">
<!-- Toggle buttons -->
<div class="layout-buttons">
<button data-layout-button="large" class="layout-btn is--active">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 12 12" fill="none" class="layout-btn__icon">
<rect x="1" y="1" width="3.33333" height="3.33333" fill="currentColor"></rect>
<rect x="1" y="7.60791" width="3.33333" height="3.33333" fill="currentColor"></rect>
<rect x="7.66797" y="1" width="3.33333" height="3.33333" fill="currentColor"></rect>
<rect x="7.66797" y="7.60791" width="3.33333" height="3.33333" fill="currentColor"></rect>
</svg>
<span class="layout-btn__label">Large</span>
</button>
<button data-layout-button="small" class="layout-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 12 12" fill="none" class="layout-btn__icon">
<rect x="1" y="1" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="4.75" y="1" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="8.5" y="1" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="1" y="4.75" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="4.75" y="4.75" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="8.5" y="4.75" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="1" y="8.5" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="4.75" y="8.5" width="2.5" height="2.5" fill="currentColor"></rect>
<rect x="8.5" y="8.5" width="2.5" height="2.5" fill="currentColor"></rect>
</svg>
<span class="layout-btn__label">Small</span>
</button>
</div>
<!-- Grid -->
<div data-layout-grid class="layout-grid">
<div data-layout-grid-collection class="layout-grid__collection">
<div data-layout-grid-list class="layout-grid__list">
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389f51914d7b695bf687_Minimalist%20Dining%20Setup.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Dining Chair</h2>
<h2 class="layout-grid__card-sub">€279</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389f4d800086249e8fbb_Minimalist%20Living%20Room.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">2-Seat Sofa</h2>
<h2 class="layout-grid__card-sub">€999</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389faac59c04a82a3391_Minimalist%20Interior%20Setup.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Lounge Chair</h2>
<h2 class="layout-grid__card-sub">€449</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389fc3b76bead9557598_Rustic%20Wooden%20Shelf.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Bookshelf</h2>
<h2 class="layout-grid__card-sub">€129</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389f9e0c6d1d3b33158c_Cozy%20Nightstand%20Setup.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Nightstand</h2>
<h2 class="layout-grid__card-sub">€249</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389f548fb9a8c6826e1a_Minimalist%20Wooden%20Desk.avif" loading="lazy" alt="" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Wooden Desk</h2>
<h2 class="layout-grid__card-sub">€549</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389f587ac1f7bf546f80_Minimalist%20Console%20Table.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Low Cabinet</h2>
<h2 class="layout-grid__card-sub">€679</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389fcaa49c34b1ce959a_Modern%20Wooden%20Sofa.avif" class="layout-grid__card-img"></div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Beige Blanket</h2>
<h2 class="layout-grid__card-sub">€49</h2>
</div>
</div>
</div>
<div data-layout-grid-item class="layout-grid__item">
<div class="layout-grid__card">
<div class="layout-grid__card-visual">
<img src="https://cdn.prod.website-files.com/68e622e47687fdcc53e30b62/68e6389fcd9a5a5b917c3e8d_Minimalist%20Wooden%20Shelf.avif" class="layout-grid__card-img">
</div>
<div class="layout-grid__card-details">
<h2 data-layout-grid-item-title class="layout-grid__card-title">Display Cupboard</h2>
<h2 class="layout-grid__card-sub">€899</h2>
</div>
</div>
</div>
</div>
</div>
</div>
</div>.layout-buttons {
grid-column-gap: .5em;
grid-row-gap: .5em;
justify-content: flex-start;
align-items: center;
padding: 1em 1em 3em;
display: flex;
}
.layout-btn {
grid-column-gap: .5em;
grid-row-gap: .5em;
background-color: #fff;
border-radius: 100em;
justify-content: flex-start;
align-items: center;
padding: .75em 1.25em;
transition: color .2s, background-color .2s;
display: flex;
}
.layout-btn.is--active {
color: #fff;
background-color: #201d1d;
}
.layout-btn__label {
font-size: 1.5em;
line-height: 1;
}
.layout-btn__icon {
width: .75em;
}
.layout-grid {
padding-bottom: 10em;
padding-left: 1em;
padding-right: 1em;
}
.layout-grid__list {
grid-row-gap: 4em;
column-gap: var(--column-gap);
flex-flow: wrap;
display: flex;
position: relative;
}
.layout-grid__item {
will-change: transform;
position: relative;
}
.layout-grid__card {
flex-flow: column;
width: 100%;
display: flex;
position: relative;
}
.layout-grid__card-visual {
aspect-ratio: 1 / 1.25;
border-radius: .25em;
overflow: hidden;
}
.layout-grid__card-img {
object-fit: cover;
width: 100%;
height: 100%;
}
.layout-grid__card-title {
margin-top: 0;
margin-bottom: 0;
font-size: 1.5em;
line-height: 1.25;
}
.layout-grid__card-sub {
opacity: .5;
margin-top: 0;
margin-bottom: 0;
font-size: 1.5em;
line-height: 1.25;
transition: opacity .2s;
}
.layout-grid__card-details {
height: 3.75em;
margin-top: .75em;
}
/* ── Layout status: column counts ── */
[data-layout-status="large"] {
--columns: 3;
--column-gap: 1.5em;
}
[data-layout-status="small"] {
--columns: 5;
--column-gap: 1em;
}
[data-layout-grid-item] {
width: calc((100% - (var(--columns) - 1) * var(--column-gap)) / var(--columns));
}
/* Title micro-motion between modes */
[data-layout-grid-item-title] {
transition: all 0.8s cubic-bezier(0.65, 0, 0.1, 1);
}
/* Price fades in delayed on large mode */
[data-layout-status="large"] [data-layout-grid-item] .layout-grid__card-sub {
transition-delay: 0.6s;
}
/* Small mode overrides */
[data-layout-status="small"] [data-layout-grid-item] .layout-grid__card-title {
font-size: 1em;
}
[data-layout-status="small"] [data-layout-grid-item] .layout-grid__card-sub {
opacity: 0;
pointer-events: none;
}
/* ── Responsive ── */
@media screen and (max-width: 767px) {
[data-layout-status="large"] {
--columns: 1;
--column-gap: 0em;
}
[data-layout-status="small"] {
--columns: 2;
--column-gap: 1em;
}
}function initGridLayoutFlip() {
const groups = document.querySelectorAll("[data-layout-group]");
const ACTIVE_CLASS = "is--active";
groups.forEach((group) => {
let activeTween = null;
const buttons = group.querySelectorAll("[data-layout-button]");
const grid = group.querySelector("[data-layout-grid]");
const collection = group.querySelector("[data-layout-grid-collection]");
if (!buttons.length || !grid || !collection) {
console.warn("Layout Grid Flip: missing required elements.");
return;
}
// Accessibility init
buttons.forEach((b) =>
b.setAttribute("aria-pressed", String(b.classList.contains(ACTIVE_CLASS)))
);
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const targetLayout = btn.getAttribute("data-layout-button");
const currentLayout = group.getAttribute("data-layout-status");
if (currentLayout === targetLayout) return;
if (activeTween) { activeTween.kill(); activeTween = null; }
// Reduced-motion: instant swap
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
group.setAttribute("data-layout-status", targetLayout);
buttons.forEach((b) => {
const isActive = b === btn;
b.classList.toggle(ACTIVE_CLASS, isActive);
b.setAttribute("aria-pressed", String(isActive));
});
window.ScrollTrigger?.refresh?.();
if (window.lenis?.resize) window.lenis.resize();
return;
}
// Capture positions before the layout change
const items = grid.querySelectorAll("[data-layout-grid-item]");
const state = Flip.getState(items, { simple: true });
collection.getBoundingClientRect();
const prevH = collection.offsetHeight;
// Apply the new layout
group.setAttribute("data-layout-status", targetLayout);
buttons.forEach((b) => {
const isActive = b === btn;
b.classList.toggle(ACTIVE_CLASS, isActive);
b.setAttribute("aria-pressed", String(isActive));
});
collection.getBoundingClientRect();
const nextH = collection.offsetHeight;
// Lock height so items can go absolute without collapsing the container
gsap.set(collection, { height: prevH });
const tl = gsap.timeline({
onStart: () => group.setAttribute("data-transitioning", "true"),
onInterrupt: () => {
group.removeAttribute("data-transitioning");
gsap.set(collection, { clearProps: "height" });
},
onComplete: () => {
group.removeAttribute("data-transitioning");
gsap.set(collection, { clearProps: "height" });
window.ScrollTrigger?.refresh?.();
if (window.lenis?.resize) window.lenis.resize();
activeTween = null;
},
});
tl
.add(Flip.from(state, {
duration: 0.65,
ease: "power4.inOut",
absolute: true,
nested: true,
prune: true,
stagger: targetLayout === "large"
? { each: 0.03, from: "end" }
: { each: 0.03, from: "start" },
}), 0)
.to(collection, {
height: nextH,
duration: 0.65,
ease: "power4.inOut",
}, 0);
activeTween = tl;
});
});
});
}
document.addEventListener('DOMContentLoaded', () => {
initGridLayoutFlip();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| [data-layout-group] | attribute | — | The outermost wrapper. The script scopes all queries here, so multiple independent instances on the same page work without interference. |
| [data-layout-status] | attribute | "large" | "small" | Declares the current layout mode on the group wrapper. The script reads this to check whether a layout change is needed and sets it to the target value before animating. CSS also reads this to apply --columns and --column-gap. |
| [data-layout-button] | attribute | "large" | "small" | Add to each toggle button. The attribute value must match the corresponding [data-layout-status] value exactly. Clicking sets the group to that layout. |
| [data-layout-grid] | attribute | — | Scopes the Flip item query to this grid element only. |
| [data-layout-grid-collection] | attribute | — | The visual wrapper around the list. The script locks its height before Flip runs and tweens it to the new height in sync, preventing container collapse during animation. |
| [data-layout-grid-list] | attribute | — | Direct wrapper of the card items. Manages wrapping and gaps via CSS custom properties. |
| [data-layout-grid-item] | attribute | — | Each card element. Recorded by Flip.getState() before the layout change and animated from its old to its new position. |
| [data-layout-grid-item-title] | attribute | — | Optional. Add to the card heading to enable CSS-driven micro-motion (e.g. font-size change) between modes. Uses a CSS transition — do not use it for changes that affect card height, as this can interfere with Flip measurements. |
| [data-transitioning] | attribute | — | Set to "true" by the script during the animation and removed on complete/interrupt. Use it in CSS to disable pointer events or add transitioning styles. |
Notes
- •Requires GSAP and the Flip plugin loaded via CDN before the script runs.
- •The attribute value on [data-layout-button] must exactly match the value used on [data-layout-status]. Mismatches will silently prevent the animation from running.
- •Avoid CSS changes that affect card or container height inside [data-layout-grid-item-title] transitions — Flip captures heights before and after the switch; unexpected height changes will cause visual glitches.
- •If ScrollTrigger or Lenis are present on the page, they are automatically refreshed after each transition completes.
- •Multiple [data-layout-group] instances on the same page are fully supported.
Guide
Column count
Define --columns and --column-gap per layout status. The card width is calculated automatically from these two variables. Add @media overrides to make the grid responsive.
[data-layout-status="large"] {
--columns: 3;
--column-gap: 1.5em;
}
[data-layout-status="small"] {
--columns: 5;
--column-gap: 1em;
}
@media screen and (max-width: 767px) {
[data-layout-status="large"] { --columns: 1; --column-gap: 0; }
[data-layout-status="small"] { --columns: 2; --column-gap: 1em; }
}Card micro-motion
Use [data-layout-grid-item-title] on the heading to add a CSS transition between modes (e.g. font-size). Avoid CSS changes that affect card height — Flip measures heights before and after the layout switch, and unexpected height changes will break the animation.
Reduced motion
When prefers-reduced-motion is active, the script skips the Flip animation and instantly applies the new layout. ScrollTrigger and Lenis are refreshed afterward in both cases.