import { motion, AnimatePresence } from 'framer-motion';
function SharedElement() {
const [selectedId, setSelectedId] = useState(null);
return (
<>
{items.map(item => (
<motion.div
layoutId={item.id}
onClick={() => setSelectedId(item.id)}
className="card-mic"
>
<motion.h5>{item.title}</motion.h5>
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<motion.div
layoutId={selectedId}
className="modal-expandat"
>
<motion.h2>{items.find(i => i.id === selectedId).title}</motion.h2>
<button onClick={() => setSelectedId(null)}>Închide</button>
</motion.div>
)}
</AnimatePresence>
</>
);
}Sincer să fiu, ani de zile m-am ferit de shared element transitions ca de dracu'. Erau frumoase în prezentările de design pe Dribbble, dar când trebuia să le implementezi în React fără să faci layout thrashing sau să te bați cu getBoundingClientRect la fiecare pas, îți venea să te lași de meserie.
Lucrurile s-au schimbat radical de când Framer Motion a introdus proprietatea layoutId. Recent, la un proiect unde aveam un dashboard cu peste 50 de widget-uri care trebuiau să se expandeze la click într-un view de detaliu, am reușit să implementez toată logica de tranziție în mai puțin de o oră. Am salvat cam 30% din timpul de dezvoltare pe care l-aș fi pierdut scriind logica custom de transformare sau luptându-mă cu CSS transitions care mereu dădeau rateuri pe dimensiuni dinamice.
De ce e layoutId un „game changer”
În mod normal, dacă vrei să animezi un element de la o poziție A la o poziție B într-un alt container, trebuie să calculezi diferența de scalare și translație. E tehnica FLIP (First, Last, Invert, Play) clasică. Framer Motion face toată matematica asta sub capotă: vede că ai un element care dispare și unul care apare cu același ID, le calculează delta-ul și aplică o transformare inversă ca să pară că e același obiect care se mișcă fluid pe ecran.
Partea cea mai mișto e că nu te obligă să muți fizic elementul în ierarhia DOM. Poți să ai un card mic într-un grid și un modal mare randat într-un Portal la rădăcina aplicației. Cât timp ambele au același layoutId, biblioteca face tranziția de la unul la altul fără să „clipească”.
Unde apar problemele și trade-off-urile
Nu e totul magie neagră și există niște capcane de care m-am lovit. La un proiect cu vreo 8k de utilizatori activi, aveam carduri foarte complexe, cu grafice SVG și mult text în interior. Când folosești layoutId, Framer aplică un scale pe containerul părinte pentru a ajunge la dimensiunea finală. Dacă elementele din interior (copiii) nu au și ei proprietatea layout, textul se va vedea turtit sau lungit inestetic în timpul animației.
Altă chestie nasoală: performanța pe Safari. Am observat un flicker de 2 frame-uri pe device-urile iOS mai vechi când modalul avea mult conținut randerat din prima. Soluția pe care am găsit-o a fost să amân randarea conținutului „greu” din modal până când animația de layout se termină, folosind evenimentul onLayoutAnimationComplete. E un mic compromis vizual, dar previne sacadarea.
Cum să nu o dai în bară la implementare
Cea mai mare greșeală pe care o văd e omiterea componentei AnimatePresence. Fără ea, elementul vechi dispare instantaneu din DOM înainte ca cel nou să poată „învăța” de unde să plece animația. De asemenea, ai grijă la border-radius. Dacă cardul tău are 8px și modalul are 0px, tranziția s-ar putea să pară ciudată dacă nu lași Framer să gestioneze el proprietatea asta (trebuie pusă în initial și animate).
Eu prefer să folosesc un spring pentru aceste tranziții. Un stiffness pe la 300 și un damping de 30 oferă acel feeling de „interfață nativă” pe care îl vezi în aplicațiile de iOS. Dacă folosești durate fixe (tween), animația pare artificială și „ieftină”.
În concluzie, layoutId e probabil cea mai rapidă cale de a ridica nivelul de polish al unui site fără să scrii sute de linii de CSS. Voi ce experiențe ați avut cu animațiile astea de layout? Preferiți să le faceți de mână pentru control total sau mergeți pe abstractizări de genul ăsta?