eduardweb.
Animații (Framer Motion)Intermediar#react#framer-motion#animatii#frontend

Cum faci Shared Element Transitions în React fără să îți pierzi mințile

De Adrian Voicu, 21 iun. 2026 · 12 vizualizări · 2 like-uri

Postat 21 iun. 2026
typescript
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

interface Card { id: string; title: string; desc: string; }

export function SharedLayout({ items }: { items: Card[] }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const activeCard = items.find(item => item.id === selectedId);

  return (
    <div className="grid grid-cols-3 gap-4">
      {items.map(item => (
        <motion.div
          layoutId={`card-${item.id}`}
          key={item.id}
          onClick={() => setSelectedId(item.id)}
          className="p-4 bg-white rounded-lg shadow cursor-pointer"
        >
          <motion.h3 layoutId={`title-${item.id}`} className="font-bold">
            {item.title}
          </motion.h3>
        </motion.div>
      ))}

      <AnimatePresence>
        {selectedId && activeCard && (
          <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
            <motion.div
              layoutId={`card-${selectedId}`}
              className="p-8 bg-white rounded-2xl max-w-lg w-full relative"
            >
              <motion.h3 layoutId={`title-${selectedId}`} className="text-2xl font-bold">
                {activeCard.title}
              </motion.h3>
              <motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-4">
                {activeCard.desc}
              </motion.p>
              <button 
                onClick={() => setSelectedId(null)} 
                className="absolute top-4 right-4 p-2 bg-gray-100 rounded-full"
              >
                X
              </button>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </div>
  );
}

Am văzut zeci de librării de animație în ultimii zece ani, dar ce face Framer Motion cu proprietatea layoutId mi se pare cel mai aproape de magie neagră. Dacă ai încercat vreodată să animezi un card mic într-un modal uriaș folosind doar CSS, știi deja cât de frustrant este să calculezi coordonate, scale-uri și opacități manual.

La un proiect recent, un dashboard cu vreo 12k utilizatori activi lunar, clientul a vrut ca la click pe un widget de raport să se deschidă un modal complet de analiză. Nu voia doar un pop-up sec, ci o tranziție fluidă în care widgetul se "extinde" pe tot ecranul. Am zis că o rezolv rapid cu layoutId de la Framer Motion și, deși rezultatul final e superb, am dat de câteva limitări pe care e bine să le știi înainte să le pui în producție.

Cum funcționează sub capotă

Framer Motion folosește tehnica FLIP (First, Last, Invert, Play). Când randezi două elemente diferite în momente diferite (sau în locuri diferite din DOM) dar le dai același layoutId, librăria își dă seama că sunt aceeași entitate conceptuală.

Calculează unde era elementul inițial, unde trebuie să ajungă, aplică un transform de inversare ca să pară că e în aceeași poziție și apoi rulează animația spre starea finală. Partea mișto e că nu trebuie să îți bați capul cu getBoundingClientRect(). Pur și simplu pui proprietatea pe ambele componente.

Capcana textului deformat și a imaginilor

Aici e trade-off-ul sincer de care te lovești rapid. layoutId animează de fapt dimensiunile fizice (lățimea și înălțimea) prin proprietatea transform: scale(). Asta înseamnă că tot ce se află în interiorul cardului tău (text, imagini, alte iconițe) va fi deformat temporar în timpul tranziției. Textul va arăta "întins" pe verticală sau orizontală timp de 200-300ms.

Cum am rezolvat asta? Sunt două soluții:

  1. Folosești proprietatea layout="position" pe elementele interne (cum ar fi tagurile de text) ca Framer Motion să le animeze doar poziția, nu și dimensiunea.
  2. Structura DOM trebuie să fie aproape identică între cardul mic și cel mare. Dacă în cardul mic ai doar un titlu, iar în cel mare ai titlu, descriere și un grafic complet, animează containerul și folosește un AnimatePresence separat pentru a aduce conținutul extra doar după ce animația s-a terminat sau cu un fade-in subtil în timpul scale-ului.

Tranziția asta a redus "friction-ul" vizual în aplicația noastră și userii au simțit imediat că platforma se mișcă mai "nativ". Totuși, mare atenție la performanță dacă ai liste imense (peste 100 de carduri randate simultan pe ecran). Framer Motion face destul de multe calcule pe layout și s-ar putea să vezi drop-uri de frame-uri pe telefoane mai vechi.

Voi ce soluții folosiți pentru shared element transitions? Rămâneți pe CSS clasic pentru performanță brută sau lăsați Framer Motion să facă magia?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.