Skip to content

FLIP Grid Reorder

A filterable grid whose tiles travel to their new positions instead of snapping.

Freeminimalplayfuleditorial
Category
Animations
Added
2026-09-20
Deps
1
Updated
2026-09-20
Preview
01
02
03
04
05
06
07
08

Props

4
320
Fade in/out
Requires framer-motion
"use client";

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";

type Item = { id: string; label: string; group: string };

export function FlipGridReorder({
  items,
  filters,
  columns = 4,
  stiffness = 320,
  fadeExit = true,
}: {
  items: Item[];
  filters: { id: string; label: string }[];
  columns?: number;
  stiffness?: number;
  /** Fade and scale tiles as they enter and leave. Off, only layout moves. */
  fadeExit?: boolean;
}) {
  const [filter, setFilter] = useState("all");
  const visible = items.filter((i) => filter === "all" || i.group === filter);

  return (
    <div className="flex flex-col gap-3">
      <div className="flex gap-1.5">
        {filters.map((f) => (
          <button
            key={f.id}
            onClick={() => setFilter(f.id)}
            aria-pressed={filter === f.id}
            className={
              "rounded-full px-3 py-1 font-mono text-[10px] uppercase tracking-[0.14em] transition-colors " +
              (filter === f.id
                ? "bg-blue-600 text-white"
                : "border border-white/20 text-white/60 hover:text-white")
            }
          >
            {f.label}
          </button>
        ))}
      </div>

      <div
        className="grid gap-2"
        style={{ gridTemplateColumns: "repeat(" + columns + ", minmax(0, 1fr))" }}
      >
        <AnimatePresence mode="popLayout" initial={false}>
          {visible.map((item) => (
            <motion.div
              /*
               * The key MUST be the item's stable id, never the array index.
               * With an index key React reuses the same DOM node for a
               * DIFFERENT item after a filter change, so Framer measures a
               * before/after box for two unrelated things — tiles appear to
               * teleport and morph instead of travelling. This one line is
               * the difference between a working FLIP grid and a broken one.
               */
              key={item.id}
              /*
               * `layout` is the whole animation: Framer records each tile's
               * box before the DOM change, reads it again after, and animates
               * the difference with a transform (FLIP). No manual measuring.
               */
              layout
              initial={fadeExit ? { opacity: 0, scale: 0.8 } : false}
              animate={{ opacity: 1, scale: 1 }}
              exit={fadeExit ? { opacity: 0, scale: 0.8 } : undefined}
              transition={{
                layout: { type: "spring", stiffness, damping: 30 },
                duration: 0.18,
              }}
              className="flex aspect-square items-center justify-center rounded-md border border-white/15 bg-white/[0.05] text-white"
            >
              {item.label}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </div>
  );
}
Notes

About this animation

Animating a filtered grid is one line of Framer — `layout` on each tile — and one line of React that almost everyone gets wrong. FLIP works by recording an element's box before a DOM change and again after, then animating the difference as a transform. That only makes sense if the before and after boxes belong to the same logical item, which is exactly what a stable key guarantees and an array index key destroys: after filtering, React happily reuses the node that was item three for what is now item one, so Framer measures two unrelated things and the tiles appear to teleport and morph rather than travel. If a reorder animation looks wrong, check the key before anything else. The second detail is `mode="popLayout"` on AnimatePresence, which pulls exiting tiles out of layout flow immediately so the survivors begin moving into the freed space while the exit is still playing — without it the grid waits, then jumps. Keep the spring firm; a floaty reorder on a dense grid reads as lag rather than motion.

Curator’s note

Built for viberdy 2.0. Written specifically around the index-key failure, which is the single most common reason people's FLIP grids look broken.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

A tile grid that flips on a diagonal wavefront to resolve into one image.

flipgrid3d

A row of stat counters that count up together the moment they scroll into view.

statscountergrid

A filterable FAQ list where each answer expands independently.

faqsearchaccordion