// Exemplu simplificat pentru Shared Element Transition
import { motion, AnimatePresence } from 'framer-motion';
function Gallery({ items }) {
const [selectedId, setSelectedId] = useState(null);
return (
<>
{items.map(item => (
<motion.div
layoutId={item.id}
onClick={() => setSelectedId(item.id)}
style={{ borderRadius: '12px', cursor: 'pointer' }}
>
<motion.img src={item.url} />
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<motion.div
layoutId={selectedId}
className="modal-overlay"
onClick={() => setSelectedId(null)}
>
<motion.div className="modal-content">
<motion.img src={items.find(i => i.id === selectedId).url} />
<button onClick={() => setSelectedId(null)}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}Hai să fim sinceri, majoritatea animațiilor de pe web sunt ori prea rigide, ori prea sacadate. Am avut de făcut un catalog de produse acum vreo șase luni pentru un client din zona de fashion și omul voia să se simtă totul „ca pe iPhone”. Știi tu, apeși pe o poză mică și aia se expandează până devine modal, fără să dispară și să apară brusc prin opacitate.
La început, am încercat s-o fac pe stilul vechi, „la mână”. Calculam getBoundingClientRect(), făceam un transform: translate manual între cele două stări, chin total. Apoi m-am prins că Framer Motion are layoutId. Mi-a scurtat timpul de implementare cu vreo 4 ore bune și codul a devenit mult mai curat. Practic, am economisit cam 30% din timpul alocat pe partea de UI polish.
Cum funcționează magia din spate
Când dai același layoutId la două componente diferite (de exemplu, un div mic într-o listă și un div mare într-un modal), Framer Motion face toată matematica pentru tine. El vede unde e elementul A, unde e elementul B, și animează diferența (delta). E genial pentru că nu trebuie să-ți pese de coordonate absolute sau de ierarhia DOM-ului.
Am observat o chestie la un proiect cu vreo 50 de carduri randate simultan: dacă nu ești atent la AnimatePresence, poți să ai mici frame drops pe telefoanele mai vechi. Pe un MacBook M1 sau un iPhone 15 nu simți, dar pe un Android de acum 3-4 ani se vede sacadat. Sfatul meu? Păstrează structura DOM-ului cât mai simplă în interiorul elementului animat.
Trade-off-uri de care m-am lovit
Nu totul e perfect, evident. Un mare minus e la imagini. Dacă imaginea din card are un aspect ratio diferit față de cea din modal (de exemplu, de la 1:1 la 16:9), o să vezi un stretch destul de urât în timpul tranziției. Framer încearcă să scaleze containerul, dar conținutul se poate deforma. Eu rezolv asta de obicei folosind layout pe un container părinte cu overflow: hidden și lăsând imaginea să ocupe spațiul liber.
Altă problemă e z-index-ul. Când elementul „pleacă” din listă spre modal, trebuie să te asiguri că e deasupra tuturor celorlalte carduri. Uneori, contextul de stivuire (stacking context) al părintelui te poate încurca groaznic. Am pierdut vreo două ore la un moment dat pentru că un overflow: auto pe un container părinte îmi tăia animația la jumătate.
Implementarea practică
E esențial să ai un ID unic. Dacă folosești product.id sau ceva similar, e perfect. Când userul dă click, schimbi un state (de exemplu, setSelectedId(id)) și randezi modalul condiționat. Framer Motion va detecta că un element cu același layoutId a apărut în DOM și va face legătura vizuală.
Pentru performanță optimă, am observat că dacă adaugi o tranziție de tip spring cu un damping mai mare (pe la 30), obții acel feeling nativ care place tuturor. Am renunțat la animațiile bazate pe durată (duration) pentru că par prea mecanice în contextul ăsta de shared elements.
Voi cum gestionați tranzițiile astea complexe? Mergeți pe Framer Motion pentru tot ce înseamnă UI sau preferați ceva mai low-level gen GSAP pentru control total pe timeline?