Smooth Shared‑Element Transitions in React with Framer Motion’s layoutId
Framer Motion’s layoutId lets you animate elements that move between UI locations—like drag‑and‑drop lists or modal openings—without manual keyframes. This blog explains the problem, feature, example, trade‑offs, and next steps.
23 Jun 2026, 14:11 UTC

Problem: Jumpy UI When Items Move
When items in a list are reordered, dragged into a modal, or swapped between tabs, the default React reconciliation simply unmounts the old component and mounts a new one. The browser therefore flashes the item in place, then instantly jumps to its new position. Users perceive this as a glitch, especially on mobile or low‑end devices.
Solution: Framer Motion’s layoutId
Framer Motion offers a single prop, layoutId, that links two components across the component tree. When the same layoutId appears in two places, the library automatically calculates the bounding rectangles of both mount points and interpolates transform and opacity between them. The result is a fluid “shared‑element” transition that feels natural and avoids manual keyframe work.
How layoutId Works
- Unique Identifier: Assign a string or number that is unique per logical item. The same value must be used on the source and destination components.
- Geometry Capture: On mount, Framer Motion records the element’s
offsetLeft,offsetTop,offsetWidth, andoffsetHeight. - Interpolation: When a matching
layoutIdappears elsewhere, the library creates atransformanimation that moves the element from its first rectangle to the second. - Performance: The animation runs on the GPU via CSS
transformandopacity, leveraging the Web Animations API orrequestAnimationFrameunder the hood. This keeps frame rates high on modern browsers.
Concrete Example: Drag‑and‑Drop Reorderable List
Below is a minimal React component that demonstrates a list of items that can be reordered by dragging. Each item is wrapped in a <motion.div> with a layoutId equal to its id. The drag prop enables physics‑based dragging, while dragConstraints keeps the item inside the list container.
import { motion } from "framer-motion";
import { useState } from "react";
const initialItems = [
{ id: "a", label: "Alpha" },
{ id: "b", label: "Bravo" },
{ id: "c", label: "Charlie" },
];
export function ReorderableList() {
const [items, setItems] = useState(initialItems);
const onDragEnd = (event, info, index) => {
const { point } = info;
const newIndex = items.findIndex((_, i) => i === index);
const target = items.findIndex(item => item.id === event.target.dataset.id);
if (target !== -1 && target !== newIndex) {
const reordered = [...items];
reordered.splice(newIndex, 1);
reordered.splice(target, 0, items[newIndex]);
setItems(reordered);
}
};
return (
<motion.ul style={{ listStyle: "none", padding: 0 }}>
{items.map((item, index) => (
<motion.li
key={item.id}
data-id={item.id}
layoutId={item.id}
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
onDragEnd={(e, info) => onDragEnd(e, info, index)}
style={{
margin: "8px 0",
padding: "12px",
background: "#f0f0f0",
borderRadius: "4px",
cursor: "grab",
}}
>
{item.label}
</motion.li>
))}
</motion.ul>
);
}
Key points:
- layoutId={item.id} links the same logical item across different positions in the list.
- drag="y" enables vertical dragging;
dragConstraintskeeps the item within the list bounds. - The
onDragEndhandler swaps items in the state array, which triggers a re‑render. Because thelayoutIdis unchanged, Framer Motion animates the geometry change smoothly.
Trade‑Offs and Limitations
- Layout Thrashing: Using
layoutIdon a large number of elements (hundreds of items) can cause the browser to recalculate layout for each frame, leading to higher CPU usage. Keep the list size reasonable or batch updates. - Conflict with CSS Filters: If the element uses
filter: blur()orperspective, the animation may not interpolate correctly because Framer Motion relies ontransformandopacity. Test such styles separately. - Memory Footprint: Each
layoutIdregistration stores bounding data for the duration of the animation. On low‑end devices, this can increase memory usage. Monitor with the browser’s Performance tab. - Browser Support: The feature depends on the Web Animations API or
requestAnimationFrame. All modern browsers support this, but very old browsers (IE11+) may fall back to less efficient CSS transitions.
Practical Verification Steps
- Render the list component in your application.
- Open Chrome DevTools, go to the Animations panel, and observe that the dragged item animates via
transformandopacity. - Use the Performance tab to record a reordering interaction. Verify that the frame rate stays above 60 fps.
- On a mobile device, repeat the test to ensure the animation remains smooth.
Next Steps for Your Project
- Start by adding
layoutIdto any component that appears in multiple places (e.g., a card that opens in a modal). - Use the
layoutprop on parent containers to enable implicit layout transitions for nested children. - If you need a custom easing or duration, pass a
transitionobject:transition={{ type: "spring", stiffness: 300 }}. - For complex scenarios, consider combining
layoutIdwithAnimatePresenceto handle exit animations when components are removed.
In short, layoutId gives you an out‑of‑the‑box shared‑element animation that feels native, performs well, and requires minimal code. By understanding its trade‑offs and verifying performance, you can confidently use it to elevate the user experience in any React application that needs fluid element transitions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.