Copied SVG to clipboard
Something went wrong
Copied code to clipboard
Something went wrong
Saved to bookmarks!
Removed from bookmarks
Webflow Challenge: Win $5K

Default

User image

Default

Name

  • -€50
    Upgrade to Lifetime
The Vault/

Layout Grid Flip

Layout Grid Flip

Documentation

Webflow

Code

Setup: External Scripts

External Scripts in Webflow

Make sure to always put the External Scripts before the Javascript step of the resource.

In this video you learn where to put these in your Webflow project? Or how to include a paid GSAP Club plugin in your project?

HTML

Copy
<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>

Step 1: Copy structure to Webflow

Copy structure to Webflow

In the video below we described how you can copy + paste the structure of this resource to your Webflow project.

Copy to Webflow

Webflow structure is not required for this resource.

Step 1: Add HTML

HTML

Copy
<div data-layout-status="large" data-layout-group class="layout-group">
  <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>
  <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>

HTML structure is not required for this resource.

Step 2: Add CSS

CSS

Copy
.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-variation-settings: "wght" 500;
  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 {
  font-variation-settings: "wght" 550;
  margin-top: 0;
  margin-bottom: 0;
  font-size: 1.5em;
  line-height: 1.25;
}

.layout-grid__card-sub {
  opacity: .5;
  font-variation-settings: "wght" 550;
  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;
}

[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));
}

[data-layout-grid-item-title]{
  transition: all 0.8s cubic-bezier(0.65, 0, 0.1, 1);
}


/* Change card layout while we're in grid mode */
[data-layout-status="large"] [data-layout-grid-item] .layout-grid__card-sub{
  transition-delay: 0.6s;
}


/* Change card layout while we're in list mode */
[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;
}


/* Define layout sizes per breakpoint */
@media screen and (max-width: 767px){
  [data-layout-status="large"]{
    --columns: 1;
    --column-gap: 0em;
  }
  
  [data-layout-status="small"]{
    --columns: 2;
    --column-gap: 1em;
  }
}

Step 2: Add custom Javascript

Custom Javascript in Webflow

In this video, Ilja gives you some guidance about using JavaScript in Webflow:

Step 2: Add Javascript

Step 3: Add Javascript

Javascript

Copy
function initGridLayoutFlip() {
  const groups = document.querySelectorAll("[data-layout-group]");
  const ACTIVE_CLASS = "is--active"; // The classes toggled on your buttons

  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("Missing required HTML elements. Check the Osmo resoure documentation!");
      return;
    }

    // a11y 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"); // "large" | "small"
        const currentLayout = group.getAttribute("data-layout-status");
        if (currentLayout === targetLayout) return;

        // Kill any in-flight animation
        if (activeTween) { activeTween.kill(); activeTween = null; }

        // Reduced-motion: just toggle and refresh
        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;
        }

        // Record state of items
        const items = grid.querySelectorAll("[data-layout-grid-item]");
        const state = Flip.getState(items, { simple: true });

        // Measure current height on the collection (force layout first)
        collection.getBoundingClientRect();
        const prevH = collection.offsetHeight;

        // Switch to target 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));
        });

        // Measure next height after switching
        collection.getBoundingClientRect();
        const nextH = collection.offsetHeight;

        // Pin collection height so items can go absolute without collapsing
        gsap.set(collection, { height: prevH });

        // Build timeline: Flip + collection height animation
        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();
});

Step 3: Add custom CSS

Step 2: Add custom CSS

Custom CSS in Webflow

Curious about where to put custom CSS in Webflow? Ilja explains it in the below video:

CSS

Copy
[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));
}

[data-layout-grid-item-title]{
  transition: all 0.8s cubic-bezier(0.65, 0, 0.1, 1);
}


/* Change card layout while we're in grid mode */
[data-layout-status="large"] [data-layout-grid-item] .layout-grid__card-sub{
  transition-delay: 0.6s;
}


/* Change card layout while we're in list mode */
[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;
}


/* Define layout sizes per breakpoint */
@media screen and (max-width: 767px){
  [data-layout-status="large"]{
    --columns: 1;
    --column-gap: 0em;
  }
  
  [data-layout-status="small"]{
    --columns: 2;
    --column-gap: 1em;
  }
}

Implementation

Container

Use [data-layout-group] on the wrapper that contains the toggle buttons and the grid, so the script scopes event listeners and Flip animations to a single instance.

Status

Use [data-layout-status="large" | "small"] on the same group wrapper to declare the current layout mode, which the script reads and switches before animating. In CSS we're also checking this status to determine the layout.

Buttons

Use [data-layout-button="large"] and [data-layout-button="small"] on your toggle buttons so the script knows which target layout to activate and animate to. Notice how the attribute value on these buttons matches the value on our container status exactly.

Grid

Use [data-layout-grid] on the element that contains the collection and list, allowing the script to query items for Flip state recording within this grid only.

Collection (height lock)

Use [data-layout-grid-collection] on the parent that visually wraps the list, because the script locks and tweens this element’s height during Flip to prevent container collapse.

List

Use [data-layout-grid-list] on the direct wrapper of items to manage wrapping and gaps, while the height tween happens on the collection above it.

Card / Item

Use [data-layout-grid-item] on each card so the script records these elements in Flip.getState() and animates their position/size changes between layouts.

Amount of columns

Use [data-layout-status="large"] and [data-layout-status="small"] to define how many columns your layout should display. Inside your CSS, these states control two custom properties: --columns and --column-gap. These are then used in a width calculation for each card. You can easily adjust these variables per breakpoint to make your layout responsive. For example, reducing columns and gaps on smaller screens:

[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;
  }
}

Title (optional micro-motion)

Use [data-layout-grid-item-title] on the heading inside a card to enable CSS-driven size tweaks between modes without interfering with Flip. You're free to add as many tweaks with this same logic as you see fit. Just be careful with CSS changes that affect the height of your card and its contents, as this can throw off the calculations happening in Flip.

Resource Details

Advanced
GSAP
Flip
Layout
Grid
List
Toggle

Original source

Ilja van Eck

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.