Architecture Note: Using Framer Motion layoutId with animatePresence for Smooth List Reordering
Learn the minimal setup, data boundaries, and checks needed to animate list reordering safely with Framer Motion's layoutId and AnimatePresence.
10 Jul 2025, 04:29 UTC

Requirements
Before adding animation, define what the UI must do:
- Render a list where each item can be removed or moved to a new index.
- When an item leaves, animate it out (fade/slide) while the remaining items shift to fill the gap.
- When an item moves, animate its position and size change smoothly without manual CSS calculations.
- Keep the animation performant: avoid layout thrash and keep frame work under ~16 ms.
Smallest Suitable Design
The minimal pieces that satisfy the requirements are:
AnimatePresenceas the direct parent of the list items – it mounts/unmounts children and runs exit animations.- Each list item rendered as a
motion.div(ormotion.li) with a stablelayoutIdderived from the item’s unique key (e.g., its ID). - A simple variant set for entrance, exit, and layout changes (the latter is implicit when
layoutIdmatches). - State that holds the ordered list of item IDs; reordering is done by immutable array operations (e.g.,
splice) triggering a render.
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
const items = [
{ id: "a", label: "First" },
{ id: "b", label: "Second" },
{ id: "c", label: "Third" },
];
export default function ReorderableList() {
const [list, setList] = useState(items.map(i => i.id));
const handleMove = (fromIdx, toIdx) => {
const [moved] = list.splice(fromIdx, 1);
list.splice(toIdx, 0, moved);
setList([...list]);
};
const variants = {
exit: { opacity: 0, y: -20, transition: { duration: 0.2 } },
};
return (
{list.map((id, idx) => {
const item = items.find(i => i.id === id);
return (
{
// simple drag‑to‑reorder logic (omitted for brevity)
const newIdx = Math.round(info.y / 40);
handleMove(idx, Math.max(0, Math.min(list.length - 1, newIdx)));
}}
>
{item.label}
);
})}
);
}
Trust and Data Boundaries
The animation system trusts two pieces of data:
- layoutId uniqueness – Framer Motion uses this to map a DOM node before and after a render. If two siblings share the same ID, the library cannot determine which element corresponds to which animation, leading to missing or incorrect transitions.
- Render hierarchy –
AnimatePresencemust be the immediate parent of the animatedmotionelements. Any non‑motion wrapper (e.g., adivfor styling) between them breaks the exit‑animation trigger because the library cannot detect the unmount of the motion child.
All other props (e.g., drag handlers, variant definitions) are treated as ordinary React props and do not affect the internal mapping.
Operational Checks
- Run the application and open the browser console. Verify that no warning appears about duplicate layoutIds.
- Use the Performance tab → Record a short drag‑reorder sequence. Look for:
- Layout shift events staying under ~2 ms per frame.
- No forced synchronous layout warnings (layout thrash).
- Visually confirm:
- Leaving items fade/slide out.
- Moving items glide to their new position without jumping.
- Entering items fade in from below.
- Optional: add a
whileHoverorwhileTapvariant to ensure the animation system remains responsive under interaction.
Failure Modes
- Duplicate layoutId – Causes undefined mapping; you may see items snap instead of animate, or console warnings in development.
- Extra wrapper between AnimatePresence and motion children – Exit animations never fire; removed items disappear instantly.
- Frequent size changes (e.g., animating height based on content) – Triggers layout thrash; the FLIP optimization still runs but each frame may force a layout, dropping frame rate.
- Using the legacy
layoutprop (pre‑v11) – In v11+ this prop is ignored, so relying on it will produce no layout animation.
Conditions That Would Change the Design
Reconsider this approach if:
- You need to animate items that are not siblings (e.g., moving between separate lists). In that case, consider
sharedLayoutgroups or a custom FLIP implementation. - The list can grow very large (hundreds of items) and you notice GC pressure from creating many
motionnodes; virtualization combined with layout animation may be required. - Your design requires simultaneous exit and entrance animations that depend on each other's timing beyond what
AnimatePresenceprovides (e.g., staggered exit/entry across lists). Then a custom orchestration withuseCycleoruseAnimationmay be clearer.
Practical verification: after implementing the snippet above, drag an item to a new position. The item should glide smoothly, the placeholder should shift, and the console should stay silent. Recording a performance profile should show layout shift times well under the 16 ms budget, confirming that the FLIP‑based layoutId animation is operating without thrash.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.