import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
type Product = { id: string; title: string; desc: string };
export function ProductCatalog({ items }: { items: Product[] }) {
const [selected, setSelected] = useState<Product | null>(null);
return (
<>
<div className="grid grid-cols-3 gap-4">
{items.map((item) => (
<motion.div
key={item.id}
layoutId={`card-${item.id}`}
onClick={() => setSelected(item)}
className="p-4 border rounded-xl cursor-pointer bg-white shadow-sm"
>
<motion.h3 layoutId={`title-${item.id}`} className="font-bold">
{item.title}
</motion.h3>
</motion.div>
))}
</div>
<AnimatePresence>
{selected && (
<div className="fixed inset-0 flex items-center justify-center bg-black/40 z-50">
<motion.div
layoutId={`card-${selected.id}`}
className="bg-white p-6 rounded-2xl max-w-md w-full shadow-2xl"
>
<motion.h3 layoutId={`title-${selected.id}`} className="text-2xl font-bold">
{selected.title}
</motion.h3>
<p className="mt-4 text-gray-600">{selected.desc}</p>
<button
onClick={() => setSelected(null)}
className="mt-6 px-4 py-2 bg-black text-white rounded-lg"
>
Închide
</button>
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
}Salutare. Am văzut destul de mulți dev-i care se chinuie ore întregi să facă un 'shared element transition' clasic — adică un card din listă care se mărește lin și devine un modal de detalii. Dacă în trecut trebuia să calculezi getBoundingClientRect(), să faci matematică pe scroll și să gestionezi z-index-ul cu CSS hacks, azi rezolvi totul cu un singur prop în Framer Motion: layoutId.
Am folosit abordarea asta recent pe un proiect de e-commerce cu peste 12k de produse vizibile în catalog. Clientul voia neapărat sentimentul ăla de aplicație nativă pe mobil, unde imaginea și titlul 'zboară' din grid direct în ecranul de produs. Am economisit lejer 2 zile de muncă și vreo 150 de linii de cod inutil pe care le-aș fi scris pentru calcule manuale.
Cum funcționează de fapt
Mecanismul din spate folosește tehnica FLIP (First, Last, Invert, Play). Când React montează componenta nouă (modalul) și o demontează pe cea veche (cardul), Framer Motion detectează că ambele au primit exact același layoutId numeric sau string.
Calculează diferența de poziție și dimensiune între starea A și starea B, apoi aplică un transform inversat pe care îl animă spre starea finală. Totul rulează pe GPU, deci prinzi lejer 60 FPS chiar și pe telefoane mid-range mai vechi.
Unde apar problemele (trade-off-uri reale)
Nu totul e lapte și miere, din păcate. M-am lovit de câteva chestii enervante în producție la care trebuie să fii atent:
- Distorsiunea de font și border-radius: Pentru că Framer Motion aplică
scaleXșiscaleYpe container în timpul tranziției, textul și colțurile rotunjite se pot alungi urât cât timp durează animația. Ca să previi asta, trebuie să puilayoutși pe elementele copil sau să foloseștimotion.h3în loc deh3simplu. - Z-Index și overflow parent: În momentul tranziției, dacă grid-ul sau containerul tău părinte are
overflow: hidden, modalul va fi tăiat direct pe margine! Soluția curată e să randezi modalul printr-un Portal îndocument.body, dar păstrândAnimatePresenceșilayoutIdintacte. - Performanța pe liste lungi: Dacă pui
layoutIdpe 200 de carduri care se randează toate deodată într-un infinite scroll, biblioteca își poate lua un mic freeze la prima montare pentru că trebuie să înregistreze toate ref-urile în memorie.
Concluzie
Dacă ai un UI modern unde vrei să conectezi vizual două stări ale aceluiași element, layoutId e indiscutabil cea mai rapidă opțiune din ecostistemul React. Economisești timp și scapi de bug-uri bizare de CSS.
Voi ce folosiți pentru shared elements pe web? V-ați lovit de distorsiuni de font când schimbați radilul sau aspect ratio-ul?