import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
export function SharedElementList() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const items = [{ id: '1', title: 'Card 1', desc: 'Descriere scurta' }];
return (
<div className="grid-layout">
{items.map(item => (
<motion.div
layoutId={`card-${item.id}`}
onClick={() => setSelectedId(item.id)}
className="card"
key={item.id}
>
{/* layout previne distorsionarea textului la scalare */}
<motion.h3 layout>{item.title}</motion.h3>
<motion.p layout>{item.desc}</motion.p>
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<div className="modal-overlay" onClick={() => setSelectedId(null)}>
<motion.div
layoutId={`card-${selectedId}`}
className="modal-content"
>
<motion.h3 layout>Product Details</motion.h3>
<motion.p layout>Text detaliat care nu se va deforma la deschidere.</motion.p>
<button onClick={() => setSelectedId(null)}>Închide</button>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}Salutare! Dacă ai încercat vreodată să scrii manual o tranziție de la un card mic dintr-o listă la un modal pe tot ecranul, știi deja ce coșmar e cu matematica din CSS. Trebuie să calculezi coordonate, să faci absolut-positioning pe undeva și să te asiguri că scroll-ul nu strică totul.
Framer Motion rezolvă asta aproape magic cu layoutId. Dai același ID la două elemente diferite din arbore și biblioteca se ocupă de restul. Totuși, în producție te lovești rapid de niște chichițe care îți pot strica complet animația.
Cum funcționează magia și unde se rupe
Am folosit schema asta pe un proiect recent, un catalog de produse cu vreo 12.000 de vizualizări pe zi. Clientul voia ca la click pe produs, cardul să se extindă fluid într-un ecran de detalii.
Sub capotă, Framer Motion folosește tehnica numită FLIP (First, Last, Invert, Play). În loc să animeze lățimea și înălțimea reale (ceea ce ar declanșa reflow în browser și ar distruge performanța), el folosește transform: scale() și translate().
Și aici apare marea problemă: distorsiunea copiilor.
Dacă ai text sau imagini în interiorul cardului care se extinde, acestea se vor întinde bizar în timpul animației. Textul va arăta deformat timp de 300ms, până când animația se termină. Este acel efect clasic de "stretch" pe care niciun designer nu îl va accepta în producție.
Soluția: Scale correction pe elementele copil
Ca să eviți deformarea textului sau a imaginilor, trebuie să îi spui explicit librăriei să aplice corecția de scală și pe elementele interne. Faci asta adăugând proprietatea layout pe copii.
Practic, dacă părintele are layoutId, copiii care trebuie să își păstreze proporțiile au nevoie de layout. Framer Motion va calcula transformarea inversă pe copii ca să anuleze efectul de stretch al părintelui.
Trade-off-uri pe care trebuie să le accepți
Nimic nu e gratis în frontend. Deși layoutId funcționează excelent în 90% din cazuri, vine la pachet cu niște compromisuri:
- Bundle size: Framer Motion adaugă în jur de 30-40kb gzipped la bundle-ul tău. Dacă ai o pagină ultra-optimizată unde fiecare kilobyte contează, s-ar putea să fie prea mult.
- Z-Index și Overflow: Când cardul se transformă în modal, se poate întâmpla ca părintele listei să aibă
overflow: hiddensau un context de stivuire (stacking context) care taie modalul. De multe ori ești obligat să folosești un Portal pentru modal, ceea ce complică puțin sincronizarea stării. - Performanța pe mobile ieftine: Pe telefoane mai vechi, dacă ai sute de elemente cu
layoutîn pagină, scroll-ul poate începe să agațe. Limitează-te doar la elementele strict necesare.
În exemplul de cod de mai jos ai structura de bază pentru un flow curat, cu AnimatePresence ca să ne asigurăm că animația de închidere se execută corect când modalul dispare din DOM.
Voi cum gestionați tranzițiile de genul ăsta? Ați trecut pe CSS View Transitions API pentru proiectele moderne sau rămâneți tot la Framer Motion pentru flexibilitate?