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

Cum faci shared element transitions curate cu layoutId în Framer Motion

De Ioana Marinescu, 4 sept. 2026 · 17 vizualizări · 2 like-uri

Postat 4 sept. 2026
typescript
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

export function ProductGallery({ items }: { items: Array<{ id: string; title: string }> }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const activeItem = items.find((i) => i.id === selectedId);

  return (
    <div className="grid grid-cols-2 gap-4">
      {items.map((item) => (
        <motion.div
          key={item.id}
          layoutId={`card-${item.id}`}
          onClick={() => setSelectedId(item.id)}
          className="cursor-pointer bg-neutral-800 p-6"
          style={{ borderRadius: 12 }}
        >
          <motion.h3 layoutId={`title-${item.id}`} className="text-white font-semibold">
            {item.title}
          </motion.h3>
        </motion.div>
      ))}

      <AnimatePresence>
        {activeItem && (
          <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
            <motion.div
              layoutId={`card-${activeItem.id}`}
              className="w-full max-w-lg bg-neutral-900 p-8 shadow-2xl"
              style={{ borderRadius: 20 }}
            >
              <motion.h2 layoutId={`title-${activeItem.id}`} className="text-2xl font-bold text-white">
                {activeItem.title}
              </motion.h2>
              <p className="mt-4 text-neutral-400">Detalii complete randate în modal...</p>
              <button onClick={() => setSelectedId(null)} className="mt-6 px-4 py-2 bg-white text-black rounded">
                Închide
              </button>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </div>
  );
}

Am refăcut recent catalogul pentru un client de e-commerce cu vreo 14.000 de SKU-uri, iar designerul visa la tranziții fluide de tip iOS: dai click pe cardul unui produs și el se expandează lin într-un modal de detalii. În CSS pur, treaba asta e o durere cruntă de cap cu getBoundingClientRect, calcule relative de viewport și transformări FLIP manuale. Cu layoutId din Framer Motion rezolvi problema în câteva linii de cod, dar dacă nu ești atent la detalii, ajungi rapid la animații distorsionate.

Cum funcționează sub capotă

Framer Motion se bazează pe tehnica FLIP (First, Last, Invert, Play). Când declanșezi tranziția și montezi modalul, biblioteca măsoară poziția inițială a cardului (First) și dimensiunile finale ale modalului (Last). Apoi aplică un transform: translate(...) scale(...) inversat pe elementul final pentru a-l face să pară că pleacă exact din locul cardului vechi, rulând apoi animația spre starea naturală.

Magia constă în faptul că cele două componente nu trebuie să fie același element în DOM. Cât timp ambele noduri primesc aceeași proprietate layoutId="card-${id}", Framer Motion știe că ele reprezintă vizual aceeași entitate și face interpolarea pe GPU fără reflow de layout.

Capcanele pe care le-am învățat pe pielea mea

Pe hârtie pare banal, dar în producție am dat de trei probleme majore:

Prima este distorsiunea conținutului. Pentru că tranziția se face prin scale, textul și imaginile din container se lățesc ciudat în timpul animației. Dacă vrei ca un titlu din card să nu arate întins pe drum spre modal, trebuie să-i pui și lui un layoutId separat (de exemplu, layoutId="title-${id}"). În plus, proprietatea layout="position" pe elementele interne ajută mult la păstrarea proporțiilor.

Al doilea obstacol a fost border-radius. Dacă aplici border-radius: 12px prin clase CSS normale (Tailwind de exemplu), acesta se scalează oribil odată cu cutia. Soluția pe care am descoperit-o după ore întregi de debugging e să pasezi borderRadius prin prop-ul style={{ borderRadius: 16 }} direct pe motion.div. Așa, Framer Motion știe să animeze raza separat pe durata scale-ului.

Al treilea aspect ține de contextul de stacking (z-index). Când expandezi un card dintr-un grid cu 20 de iteme, ai surpriza ca modalul aflat în tranziție să treacă pe sub cardul următor din listă. E musai ca wrapper-ul modalului să aibă un z-index mare și să fie randat preferabil printr-un AnimatePresence plasat la un nivel superior în arbore sau direct într-un Portal.

Trade-off sincer: când merită și când nu

layoutId e genial pentru carduri de galerie, articole de blog, ferestre de preview sau widget-uri de dashboard unde vrei senzația aia nativă, fluidă. Am economisit sute de linii de logică pe care le scriam manual în trecut.

În schimb, e o idee proastă pe tabele mari de date sau pe liste infinite fără virtualizare. Când ai peste 80-100 de elemente complexe cu layoutId randate simultan, Framer Motion începe să facă măsurători geometrice care pot produce frame drops pe telefoane mid-range. Dacă modalul conține formulare grele sau iframe-uri, mai bine mergi pe un clasic fade-in cu backdrop.

Voi folosiți layoutId în aplicații complexe de producție sau preferați să rămâneți la modalele clasice cu overlay simplu?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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