import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
export default function SharedLayout() {
const [activeId, setActiveId] = useState<string | null>(null);
const items = [
{ id: "1", title: "Card Cursuri", desc: "Detalii despre cursuri..." },
{ id: "2", title: "Card Profil", desc: "Setările contului tău..." }
];
return (
<div className="flex gap-4 p-8">
{items.map((item) => (
<motion.div
key={item.id}
layoutId={`card-${item.id}`}
onClick={() => setActiveId(item.id)}
className="cursor-pointer p-6 bg-slate-800 text-white rounded-xl"
>
<motion.h3 layoutId={`title-${item.id}`} className="font-bold">
{item.title}
</motion.h3>
</motion.div>
))}
<AnimatePresence>
{activeId && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<motion.div
layoutId={`card-${activeId}`}
className="bg-slate-900 text-white p-8 rounded-2xl w-full max-w-md"
>
<motion.h3 layoutId={`title-${activeId}`} className="text-2xl font-bold">
{items.find(i => i.id === activeId)?.title}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-4 text-slate-300"
>
{items.find(i => i.id === activeId)?.desc}
</motion.p>
<button
onClick={() => setActiveId(null)}
className="mt-6 px-4 py-2 bg-red-500 rounded"
>
Închide
</button>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}Dacă ai încercat vreodată să animezi manual un card mic într-un modal mare folosind CSS pur, probabil ai vrut să-ți schimbi meseria după două ore de calculat coordonate pe ecran. Framer Motion are un mic „cheat code” numit layoutId care se ocupă de toată matematica asta în spate. Am implementat treaba asta acum câteva luni pe o platformă de e-learning cu vreo 6.000 de utilizatori activi și feedback-ul pe partea de UX a fost excelent, fără să scriu sute de linii de JS complex.
Să vedem cum funcționează, unde se împiedică majoritatea devilor și cum o rezolvi elegant.
Cum funcționează magia din spate?
Framer Motion folosește sub capotă tehnica FLIP (First, Last, Invert, Play). Când randezi un element nou (modalul) care are același layoutId ca cel vechi (cardul), biblioteca face următoarele:
- Calculează poziția și dimensiunea inițială a cardului (First).
- Calculează poziția și dimensiunea finală a modalului (Last).
- Aplică o transformare de scalare și translație inversă ca să facă modalul să pară că ocupă exact spațiul cardului inițial (Invert).
- Rulează animația spre valorile naturale (Play).
Partea mișto este că nu trebuie să-ți pese de absolute positioning, de top, left sau de calcule matematice complicate. React știe doar că un element a dispărut din DOM și altul a apărut în alt loc, dar vizual tranziția este perfect fluidă.
Unde se rupe filmul: Trade-off-urile reale
Deși pare magie curată, în producție te lovești rapid de câteva probleme destul de enervante.
1. Distorsiunea textului și a imaginilor
Pentru că Framer Motion folosește transform: scale pe container pentru a face tranziția rapidă, tot ce este în interiorul containerului (text, imagini, butoane) se va deforma ciudat în timpul animației. Textul va părea pixelat sau întins pentru o fracțiune de secundă.
Cum o rezolvi: Trebuie să aplici proprietatea layout și pe elementele din interior (cum ar fi titlurile) sau, și mai bine, să le faci un fade-in/fade-out simplu (opacitate de la 0 la 1) în timp ce containerul principal se mărește.
2. Performanța pe telefoane ieftine
Am observat asta pe un proiect de e-commerce: pe un iPhone de ultimă generație tranziția rulează la 120Hz, dar pe un Android low-end agață groaznic. Dacă modalul tău randează instant un arbore masiv de DOM (hărți, tabele, 50 de imagini), browserul va încerca să animeze tot acest conținut în timp ce se scalează.
Soluția mea: Randează doar scheletul (un skeleton loader simplu) în timpul animației. Încarcă conținutul greu din punct de vedere randare abia după ce animația s-a terminat, folosind callback-ul onLayoutAnimationComplete.
Voi cum abordați tranzițiile de genul ăsta în aplicațiile voastre? Mergeți pe librării de animație dedicate sau preferați micro-interacțiunile native scrise de mână?