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

Cum rezolvi shared element transitions curate în React cu layoutId

De Radu Grigore, 14 oct. 2026 · 6 vizualizări · 3 like-uri

Postat acum 2 zile
typescript
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

export function CardModal({ 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="p-4 bg-white rounded-xl shadow cursor-pointer"
        >
          <motion.h3 layoutId={`title-${item.id}`}>{item.title}</motion.h3>
        </motion.div>
      ))}

      <AnimatePresence>
        {activeItem && (
          <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
            <motion.div
              layoutId={`card-${activeItem.id}`}
              className="w-full max-w-lg p-6 bg-white rounded-2xl shadow-2xl"
            >
              <motion.h3 layoutId={`title-${activeItem.id}`} className="text-xl font-bold">
                {activeItem.title}
              </motion.h3>
              <p className="mt-4 text-gray-600">Conținut detaliat încărcat în modal...</p>
              <button onClick={() => setSelectedId(null)} className="mt-6 px-4 py-2 bg-gray-100 rounded-lg">
                Închide
              </button>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </div>
  );
}

Tranzițiile de tip shared element erau un coșmar acum câțiva ani dacă trebuia să le scrii de mână. Trebuia să calculezi getBoundingClientRect pe card, să iei dimensiunile modalului destinație, să aplici un transform invers și abia apoi să animezi pe tehnica FLIP.

Cu Framer Motion scapi de toată matematica asta chinuitoare folosind un singur prop: layoutId. La un catalog de produse cu vreo 40 de item-uri pe pagină, am trecut de la un modal abrupt la o tranziție fluidă de card-to-modal în mai puțin de o oră de refactor. Rezultatul arată nativ, ca pe iOS, dar există vreo două detalii tehnice pe care documentația oficială le cam trece cu vederea.

Cum funcționează sub capotă

Când două componente randate în momente diferite sau în arbori de DOM diferiți au același layoutId, Framer Motion calculează automat diferența de poziție și dimensiune dintre ele. În loc să monteze noul element brusc și să-l demonteze pe cel vechi, biblioteca interpolează proprietățile de layout direct pe GPU prin transformări de tip scale și translate.

Practic, când selectedId devine activ în starea ta, cardul original rămâne acolo (sau se ascunde discret), iar modalul preia geometria lui de pornire și face tranziția către poziția finală centrată pe ecran.

Marea capcană: deformarea conținutului (scale distortion)

Pentru că Framer Motion folosește transform: scale() pentru a optimiza performanța și a ține animația la 60fps, copiii elementului tău vor fi deformați în timpul tranziției. Textul se lățește urât, iar colțurile rotunjite își pierd proporția.

Ca să eviți asta, ai două reguli de aur:

  1. Prop-ul layout pe elementele text interioare: Dacă ai un titlu în card care ajunge și în modal, pune-i și lui un layoutId distinct (de exemplu, layoutId={title-${item.id}}). Asta îi spune motorului de animație să recalculeze invers scalarea pentru text.
  2. Stilizarea borderRadius direct în prop-ul style sau ca motion prop: Nu pune clase CSS statice de tip rounded-2xl pentru raze diferite pe card versus modal. Dacă pe card vrei 12px și pe modal vrei 24px, pasează-le prin animate={{ borderRadius: 24 }}. Altfel, CSS-ul va face salt brusc în mijlocul animației.

Când merită și când devine o bătaie de cap

Merge brici pentru galerii, carduri de dashboard care devin formulare de detaliu și liste de produse. Utilizatorul percepe aplicația ca fiind mult mai rapidă pentru că ochiul urmărește elementul, nu un ecran alb care se blochează o fracțiune de secundă.

Unde devine nasol? Dacă modalul tău conține liste infinite, grafice complexe de Recharts sau hărți. Randarea inițială a acelor componente grele în interiorul unui element animat va provoca frame drops chiar la startul tranziției. Soluția pe care am mers eu a fost să afișez întâi un skeleton minimalist în interiorul modalului și să montez datele grele doar pe callback-ul onAnimationComplete.

Voi cum abordați tranzițiile astea când aveți mult conținut greu în modal? Încărcați asincron după tranziție sau preferați un pop-up clasic fără animații legate?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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