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

Tranziții magice de la card la modal cu layoutId în Framer Motion

De Răzvan Matei, 3 iun. 2026 · 2 vizualizări · 3 like-uri

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

export default function SharedLayout() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const items = [
    { id: "card-1", title: "Performanță", subtitle: "Optimizare React" },
    { id: "card-2", title: "Animații", subtitle: "Framer Motion" }
  ];

  return (
    <div className="grid grid-cols-2 gap-4 p-6">
      {items.map(item => (
        <motion.div
          layoutId={item.id}
          key={item.id}
          onClick={() => setSelectedId(item.id)}
          className="p-6 bg-gray-100 rounded-2xl cursor-pointer"
        >
          <motion.h5 className="text-sm text-gray-500">{item.subtitle}</motion.h5>
          <motion.h2 className="text-xl font-bold">{item.title}</motion.h2>
        </motion.div>
      ))}

      <AnimatePresence>
        {selectedId && (
          <div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
            {items.filter(item => item.id === selectedId).map(item => (
              <motion.div
                layoutId={selectedId}
                key={selectedId}
                className="p-10 bg-white rounded-3xl w-full max-w-md shadow-2xl"
              >
                <motion.h5 className="text-sm text-blue-500">{item.subtitle}</motion.h5>
                <motion.h2 className="text-3xl font-bold mb-4">{item.title}</motion.h2>
                <p className="text-gray-600">Conținut detaliat randat fluid la expandare.</p>
                <button
                  onClick={(e) => { e.stopPropagation(); setSelectedId(null); }}
                  className="mt-6 px-4 py-2 bg-red-500 text-white rounded-xl"
                >
                  Închide
                </button>
              </motion.div>
            ))}
          </div>
        )}
      </AnimatePresence>
    </div>
  );
}

Am implementat recent o tranziție de la un card mic la un modal detaliat folosind layoutId din Framer Motion. Chestia asta pare magie pură când funcționează, dar te poate face să-ți smulgi părul din cap dacă nu înțelegi cum își face React reconcilierea sub capotă. Hai să vedem cum o faci corect și unde se împiedică majoritatea devilor.

Cum funcționează magia?

La un proiect recent cu vreo 12k utilizatori activi pe lună, designerul a venit cu o cerință clasică: utilizatorul dă click pe un card dintr-o listă, iar cardul respectiv se mărește fluid până devine un modal pe tot ecranul. Fără salturi brute, fără layout shifts ciudate.

În mod normal, trebuia să calculezi manual coordonatele cu getBoundingClientRect, să faci magii cu transformări CSS și să te rogi să nu se modifice layout-ul în spate. Framer Motion rezolvă asta prin tehnica FLIP (First, Last, Invert, Play) integrată direct. Tot ce trebuie să faci este să pui același layoutId pe ambele elemente — cel de pornire (cardul mic) și cel de sosire (modalul expandat). React randează două componente complet diferite în momente diferite, dar Framer Motion le "lipește" vizual printr-o animație de interpolare.

Implementarea concretă

Ideea de bază e simplă: ai o stare selectedId în componenta părinte. Când dai click pe card, setezi ID-ul respectiv. Când selectedId nu este null, randezi modalul deasupra, înfășurat într-un AnimatePresence pentru a permite animația de ieșire (exit intent).

În codul de mai jos, observă cum ambele elemente (motion.div-ul din listă și cel din modal) împart exact același layoutId. Framer Motion se ocupă de restul. Face tranziția inclusiv pentru text, fundal și dimensiuni.

Capcanele de care m-am lovit pe producție

Sună prea frumos ca să nu aibă bube, nu? Ei bine, shared layout-ul are niște limite destul de enervante de care m-am lovit direct:

  1. Performanța pe telefoane ieftine: Dacă în interiorul modalului ai imagini mari sau text mult care se reformatează în timpul animației, o să ai un lag vizibil (drop de cadre sub 30 FPS). Soluția mea a fost să animez doar containerul (folosind layoutId), iar conținutul detaliat să-l încarc cu un delay mic, după ce animația s-a terminat, folosind callback-ul onLayoutAnimationComplete.
  2. Propagarea click-urilor: Când apeși pe butonul de închidere din modal, ai grijă să folosești e.stopPropagation(). Altfel, riști să declanșezi din nou evenimentul de click de pe cardul din spate, blocând utilizatorul într-o buclă infinită.
  3. Deformarea textului (Font stretching): Framer Motion scalează elementul ca pe o imagine în timpul tranziției. Asta înseamnă că textul se va lăți ciudat pentru câteva milisecunde. Ca să eviți asta, pune proprietatea layout și pe elementele de tip text (motion.h2, motion.p), nu doar pe container.

Cum vi se pare abordarea asta? Rămâneți la CSS transitions clasice sau riscați cu Framer Motion în producție pentru un UX de top?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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