All components

Creative Clutter Flip Layouts

GSAP animation component · Published 2026-07-21 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

A messy-desk hero of 11 floating cut-out items scattered around a centered serif header, with three arrangements (chaos scatter, tidy cleanup grid, notebook cluster) switched via bottom icon buttons. Each switch captures Flip.getState, applies new percent-based positions and rotations with gsap.set, then animates with Flip.from (1.25s, power3.inOut, 0.1s center-out stagger, absolute); the layout recalculates on window resize.

How it's built

Categoryinteractive
Techgsap
GSAP pluginsFlip
Complexitysection
Performance costlight
Mobile-safeyes

flip layout-switch collage desk stagger grid buttons

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Creative Clutter — Flip Layout Switch

Goal

Build a full-viewport, light "messy desk" hero: 11 cut-out desk objects (music player card, CD, error dialog, folder icon, mini computer, ruled paper, passport, portrait poster, app icon, lighter, cursor) float at scattered positions and angles around a centered serif headline block. Three small icon buttons at the bottom switch between three named arrangementschaos (a random-looking scatter), cleanup (a tidier, un-rotated spread with the header pushed to the right), and notebook (a tight cluster). The star effect: on each button press, GSAP Flip captures the current layout and smoothly morphs every object AND the header from its old position/size/rotation to the new one, with a slow power3.inOut ease and a center-out stagger, so the whole desk re-organizes itself in one fluid choreographed move.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugin Flip (imported from gsap/all and registered with gsap.registerPlugin(Flip)). No smooth-scroll, no other libraries. Ship one index.html (<link rel="stylesheet" href="./styles.css"> and <script type="module" src="./script.js">), one styles.css, one ES-module script.js. Must run in a fresh Vite + npm project.

Layout / HTML

<section class="desk">
  <div class="header">
    <h1>Creative Clutter</h1>
    <p>The best ideas live somewhere between a coffee stain and a half-open
       folder, scattered things have a way of finding others when you stop
       trying to organize.</p>
  </div>

  <div class="item" id="music"><img src="<music player card>" /></div>
  <div class="item" id="cd"><img src="<compact disc>" /></div>
  <div class="item" id="dialog"><img src="<retro error dialog>" /></div>
  <div class="item" id="folder"><img src="<folder icon>" /></div>
  <div class="item" id="macmini"><img src="<mini desktop computer>" /></div>
  <div class="item" id="paper"><img src="<ruled notebook sheet>" /></div>
  <div class="item" id="passport"><img src="<passport booklet>" /></div>
  <div class="item" id="portrait"><img src="<framed portrait poster>" /></div>
  <div class="item" id="appicon"><img src="<rounded-square app icon>" /></div>
  <div class="item" id="lighter"><img src="<disposable lighter>" /></div>
  <div class="item" id="cursor"><img src="<arrow cursor + spinner>" /></div>

  <div class="modes">
    <button class="active" data-mode="chaos" aria-label="Chaos mode"><svg…/></button>
    <button data-mode="cleanup" aria-label="Cleanup mode"><svg…/></button>
    <button data-mode="notebook" aria-label="Notebook mode"><svg…/></button>
  </div>
</section>
  • Every draggable object is a .item with a unique id (music, cd, dialog, folder, macmini, paper, passport, portrait, appicon, lighter, cursor) — the JS keys sizes and per-mode positions off these ids.
  • .modes holds exactly three <button> elements, each with a data-mode of chaos / cleanup / notebook. The first (chaos) starts with class active. Icons are inline monoline SVGs (stroke="currentColor", stroke-width="32", viewBox 0 0 512 512): a lightning bolt for chaos, a 2×2 rounded-square grid for cleanup, an open book for notebook.

Styling

Fonts (Google Fonts): body "DM Sans"; headline "Instrument Serif".

Palette (CSS vars on :root)

  • --base-100: #f5f2ed (warm off-white page background)
  • --base-200: #e0dfd7 (button fill/border)
  • --base-300: #5f5f5f (muted text / icon color)
  • --base-400: #171717 (near-black text)

Reset: * { margin:0; padding:0; box-sizing:border-box; }. body { font-family:"DM Sans"; background:var(--base-100); color:var(--base-400); }. img { width:100%; height:100%; object-fit:contain; } (so each object keeps its own aspect ratio, centered inside its square box).

Stage.desk { position:relative; width:100%; height:100svh; max-width:1400px; margin:0 auto; }. This element's measured offsetWidth/offsetHeight drive all position math.

Header.header { position:absolute; width:400px; text-align:center; display:flex; flex-direction:column; gap:0.75rem; pointer-events:none; z-index:10; }.

  • .header h1 { font-family:"Instrument Serif"; font-size:4rem; font-weight:600; letter-spacing:-0.02rem; line-height:1; }
  • .header p { font-size:0.95rem; line-height:1.75; color:var(--base-300); }

Items.item { position:absolute; will-change:top,left,transform; }. They are positioned entirely by GSAP via inline transforms + inline width/height (see below), never by CSS coordinates.

Mode buttons.modes { position:fixed; bottom:7.5svh; left:50%; transform:translateX(-50%); display:flex; gap:0.5rem; z-index:10; }.

  • .modes button { width:3rem; height:3rem; font-size:1.25rem; color:var(--base-300); background:var(--base-100); border:1px solid var(--base-200); border-radius:0.5rem; display:flex; justify-content:center; align-items:center; transition:all 500ms ease; cursor:pointer; }
  • .modes button:active { transform:scale(0.9); }
  • .modes button.active { background:var(--base-200); border:1px solid var(--base-200); }

Responsive

  • @media (max-width:1400px) { .desk { overflow-x:hidden; } }
  • @media (max-width:1000px): overlay a semi-transparent wash with .desk::after { content:""; position:absolute; inset:0; width:100%; height:100%; background-color:rgba(245,242,237,0.5); }, and desaturate the objects with .item { filter:saturate(0); }.

GSAP effect (be exhaustive)

Plugin & targets
import gsap from "gsap";
import { Flip } from "gsap/all";
gsap.registerPlugin(Flip);

const desk    = document.querySelector(".desk");
const header  = document.querySelector(".header");
const items   = gsap.utils.toArray(".item");   // the 11 objects
const flipTargets = [header, ...items];         // 12 elements flip together
const switches = document.querySelectorAll(".modes button");
let activeMode = "chaos";
Object sizes (square box, px — used as both width and height)
music: 325   appicon: 100   cd: 400    cursor: 125   dialog: 300   folder: 150
lighter: 225   macmini: 250   paper: 375   passport: 250   portrait: 375

Each object's box is square; object-fit:contain letterboxes the real image inside it.

Arrangements (per-mode data)

Positions are percentages of the desk's width (x) and height (y), applied as gsap.set translate offsets (top-left of the box lands at that percent point; values can be negative or >100 so boxes bleed off-stage). rotation in degrees. The header also carries x/y percentages plus a center flag.

chaosheader {x:50, y:47.5, center:true}

music    x:-2.5  y:-2.5  r:-15      appicon  x:20    y:15    r:5
cd       x:72.5  y:5     r:0        cursor   x:72.5  y:75    r:0
dialog   x:80    y:60    r:15       folder   x:90    y:50    r:5
lighter  x:2.5   y:45    r:-10      macmini  x:9.5   y:55    r:15
paper    x:5     y:15    r:10       passport x:-2.5  y:65    r:-35
portrait x:65    y:20    r:-5

cleanupheader {x:70, y:37.5, center:false} (header shifts right, no center-offset, all objects un-rotated)

music    x:76.5  y:-5    r:0        appicon  x:64.5  y:6     r:0
cd       x:0     y:47.5  r:0        cursor   x:63.5  y:23    r:0
dialog   x:34.5  y:59    r:0        folder   x:24.5  y:33    r:0
lighter  x:-6    y:3.5   r:0        macmini  x:82.5  y:66    r:0
paper    x:9     y:-3.5  r:0        passport x:60    y:65.5  r:0
portrait x:36.5  y:5.5   r:0

notebookheader {x:50, y:47.5, center:true} (tight, tilted cluster)

music    x:45    y:0.5   r:20       appicon  x:65    y:70    r:25
cd       x:27.5  y:15    r:10       cursor   x:75    y:35    r:0
dialog   x:30    y:57.5  r:10       folder   x:25    y:40    r:10
lighter  x:30    y:7.5   r:30       macmini  x:50    y:50    r:-5
paper    x:10    y:10    r:-30      passport x:16.5  y:50    r:-20
portrait x:57.5  y:20    r:10
setLayout(mode) — writes the target layout instantly (no animation)
function setLayout(mode) {
  const deskWidth  = desk.offsetWidth;
  const deskHeight = desk.offsetHeight;
  const layout     = arrangements[mode];
  const isMobile   = deskWidth < 1000;

  // header offset: subtract half its own size only when centered (or always on mobile)
  const offsetX = isMobile ? header.offsetWidth  / 2
                : layout.header.center ? header.offsetWidth  / 2 : 0;
  const offsetY = isMobile ? header.offsetHeight / 2
                : layout.header.center ? header.offsetHeight / 2 : 0;
  const headerX = isMobile ? 50   : layout.header.x;
  const headerY = isMobile ? 47.5 : layout.header.y;

  gsap.set(header, {
    x: (headerX / 100) * deskWidth  - offsetX,
    y: (headerY / 100) * deskHeight - offsetY,
    rotation: 0,
  });

  layout.items.forEach(({ id, x, y, rotation }) => {
    gsap.set(`#${id}`, {
      x: (x / 100) * deskWidth,
      y: (y / 100) * deskHeight,
      width:  itemSizes[id],
      height: itemSizes[id],
      rotation,
    });
  });
}

Notes: the header's percent point is (x,y), and when center:true (chaos, notebook) we subtract half the header's own width/height so it is centered on that point; in cleanup (center:false) the header's top-left anchors at the point instead, sliding it toward the right. On mobile (deskWidth < 1000) the header is forced to the centered (50, 47.5) position regardless of mode.

switchMode(mode) — the Flip morph (the star effect)
function switchMode(mode) {
  if (mode === activeMode) return;
  const state = Flip.getState(flipTargets);   // record current position/size/rotation of all 12
  setLayout(mode);                            // jump them to the new layout instantly
  Flip.from(state, {
    duration: 1.25,
    ease: "power3.inOut",
    stagger: { amount: 0.1, from: "center" },
    absolute: true,
  });
  activeMode = mode;
}
  • Mechanism: Flip.getState snapshots the header + all 11 objects. setLayout mutates them to the destination layout in a single frame. Flip.from(state, …) then plays the animation from the recorded snapshot to the now-current layout, tweening x, y, width, height, and rotation for each element simultaneously.
  • duration: 1.25 s. ease: "power3.inOut" (slow start, fast middle, slow settle). stagger: { amount: 0.1, from: "center" } — total 0.1 s of stagger spread across the 12 targets, radiating out from the center of the array so the middle objects lead and the outer ones follow. absolute: true — Flip pins each element to position:absolute for the duration so the staggered moves don't reflow each other.
  • No opacity/scale keyframes; the entire visual change is position + size + rotation interpolation.
Wiring
setLayout("chaos");   // initial paint, no animation

switches.forEach((btn) => {
  btn.addEventListener("click", () => {
    switches.forEach((b) => b.classList.remove("active"));
    btn.classList.add("active");
    switchMode(btn.dataset.mode);
  });
});

window.addEventListener("resize", () => setLayout(activeMode));

On resize, setLayout(activeMode) re-runs to re-derive pixel positions from the new desk size — instant, no Flip animation (only mode-button clicks trigger the Flip morph).

Assets / images

11 cut-out objects, each a PNG on a transparent background, framed roughly to fit inside a square box (the box side equals the size listed above; images are object-fit:contain, so real aspect ratios vary):

  1. music — a light, minimal "now playing" music-player UI card with album art, track title, progress bar and playback controls.
  2. cd — a shiny silver compact disc, top-down, with rainbow light refractions.
  3. dialog — a small retro OS-style error dialog window with a title bar, warning icon and an OK button.
  4. folder — a plain glossy OS-style folder icon in flat teal / turquoise with a soft rounded tab, no emblem or label, and a faint magenta rim glow (roughly landscape, slightly wider than tall).
  5. macmini — a compact silver / aluminium square desktop-computer unit seen from a top-front 3/4 angle, with a small dark logo emblem centered on its top face and a subtle magenta edge glow (near-square footprint).
  6. paper — a neat stack of blank white printer/paper sheets seen from a 3/4 isometric angle, edges lightly defined, with a faint magenta rim shadow (landscape crop, clearly wider than tall).
  7. passport — a brown leather passport booklet with embossed lettering and a small silver compass resting on it.
  8. portrait — an illustrated stylized side-profile portrait poster of a person, with a white border and soft drop shadow (reads like a framed print).
  9. appicon — a near-black / charcoal rounded-square app icon inside a bright silver-white frame, filled with thin white geometric guide lines (concentric circles, diagonals and a grid), wrapped in a soft pink/magenta outer glow (square).
  10. lighter — a green disposable pocket lighter standing upright, with a brushed-silver metal hood and a small red ignition button (tall portrait crop, much taller than wide).
  11. cursor — a black arrow cursor next to a rainbow spinning wait-cursor / beachball.

No client or third-party branding anywhere; keep the sticky-note text neutral.

Behavior notes

  • Desktop-first: the scatter is tuned for wide viewports; below 1000px the objects desaturate (filter:saturate(0)), a translucent wash is laid over the desk, and the header is force-centered.
  • Layout is fully derived from percentages of the live desk size, so it stays correctly placed on every resize.
  • The Flip morph fires only on a mode-button click and is a no-op if you click the already-active mode. Repeated switching between the three modes should always animate cleanly because Flip.getState re-reads the true current layout each time.