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

Tranziții fluide de la Card la Modal cu layoutId în Framer Motion

De Florin Manea, 7 aug. 2026 · 8 vizualizări · 2 like-uri

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

type Product = { id: string; title: string; desc: string };

export function ProductCatalog({ items }: { items: Product[] }) {
  const [selected, setSelected] = useState<Product | null>(null);

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

      <AnimatePresence>
        {selected && (
          <div className="fixed inset-0 flex items-center justify-center bg-black/40 z-50">
            <motion.div
              layoutId={`card-${selected.id}`}
              className="bg-white p-6 rounded-2xl max-w-md w-full shadow-2xl"
            >
              <motion.h3 layoutId={`title-${selected.id}`} className="text-2xl font-bold">
                {selected.title}
              </motion.h3>
              <p className="mt-4 text-gray-600">{selected.desc}</p>
              <button
                onClick={() => setSelected(null)}
                className="mt-6 px-4 py-2 bg-black text-white rounded-lg"
              >
                Închide
              </button>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </>
  );
}

Salutare. Am văzut destul de mulți dev-i care se chinuie ore întregi să facă un 'shared element transition' clasic — adică un card din listă care se mărește lin și devine un modal de detalii. Dacă în trecut trebuia să calculezi getBoundingClientRect(), să faci matematică pe scroll și să gestionezi z-index-ul cu CSS hacks, azi rezolvi totul cu un singur prop în Framer Motion: layoutId.

Am folosit abordarea asta recent pe un proiect de e-commerce cu peste 12k de produse vizibile în catalog. Clientul voia neapărat sentimentul ăla de aplicație nativă pe mobil, unde imaginea și titlul 'zboară' din grid direct în ecranul de produs. Am economisit lejer 2 zile de muncă și vreo 150 de linii de cod inutil pe care le-aș fi scris pentru calcule manuale.

Cum funcționează de fapt

Mecanismul din spate folosește tehnica FLIP (First, Last, Invert, Play). Când React montează componenta nouă (modalul) și o demontează pe cea veche (cardul), Framer Motion detectează că ambele au primit exact același layoutId numeric sau string.

Calculează diferența de poziție și dimensiune între starea A și starea B, apoi aplică un transform inversat pe care îl animă spre starea finală. Totul rulează pe GPU, deci prinzi lejer 60 FPS chiar și pe telefoane mid-range mai vechi.

Unde apar problemele (trade-off-uri reale)

Nu totul e lapte și miere, din păcate. M-am lovit de câteva chestii enervante în producție la care trebuie să fii atent:

  1. Distorsiunea de font și border-radius: Pentru că Framer Motion aplică scaleX și scaleY pe container în timpul tranziției, textul și colțurile rotunjite se pot alungi urât cât timp durează animația. Ca să previi asta, trebuie să pui layout și pe elementele copil sau să folosești motion.h3 în loc de h3 simplu.
  2. Z-Index și overflow parent: În momentul tranziției, dacă grid-ul sau containerul tău părinte are overflow: hidden, modalul va fi tăiat direct pe margine! Soluția curată e să randezi modalul printr-un Portal în document.body, dar păstrând AnimatePresence și layoutId intacte.
  3. Performanța pe liste lungi: Dacă pui layoutId pe 200 de carduri care se randează toate deodată într-un infinite scroll, biblioteca își poate lua un mic freeze la prima montare pentru că trebuie să înregistreze toate ref-urile în memorie.

Concluzie

Dacă ai un UI modern unde vrei să conectezi vizual două stări ale aceluiași element, layoutId e indiscutabil cea mai rapidă opțiune din ecostistemul React. Economisești timp și scapi de bug-uri bizare de CSS.

Voi ce folosiți pentru shared elements pe web? V-ați lovit de distorsiuni de font când schimbați radilul sau aspect ratio-ul?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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