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

Tranziții fluide cu Framer Motion layoutId: De la card la modal fără bătăi de cap

De Elena Dumitrescu, 30 iul. 2026 · 6 vizualizări · 3 like-uri

Postat 30 iul. 2026
tsx
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

export function CourseCard({ id, title, image }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <motion.div
        layoutId={`card-container-${id}`}
        onClick={() => setIsOpen(true)}
        className="card"
      >
        <motion.img layoutId={`card-image-${id}`} src={image} alt={title} />
        <motion.h3>{title}</motion.h3>
      </motion.div>

      <AnimatePresence>
        {isOpen && (
          <div className="overlay">
            <motion.div
              layoutId={`card-container-${id}`}
              className="modal"
            >
              <motion.img layoutId={`card-image-${id}`} src={image} alt={title} />
              <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
                <p>Conținut detaliat care nu se distorsionează la scalare.</p>
                <button onClick={() => setIsOpen(false)}>Închide</button>
              </motion.div>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </>
  );
}

Am refăcut recent dashboard-ul pentru un client din zona de e-learning (în jur de 12k utilizatori activi lunar) și am vrut să scăpăm de clasicele modale care apar brusc pe ecran. Voiam ca imaginea de pe cardul de curs să se extindă lin în header-ul modalului, oferind acea senzație de aplicație nativă de iOS.

Dacă ai încercat vreodată să faci asta manual în CSS cu getBoundingClientRect() și tranziții calculate pe transform, știi deja ce coșmar matematic te așteaptă, mai ales pe ecrane responsive. Aici intră în scenă layoutId din Framer Motion. În 15 minute ai rezolvat problema, dar există câteva trade-off-uri și detalii tehnice pe care e bine să le știi înainte să dai push în producție.

Cum funcționează FLIP sub capotă

Secretul din spatele layoutId este tehnica FLIP (First, Last, Invert, Play). Când starea ta se schimbă și React randează modalul în locul cardului, Framer Motion măsoară poziția și dimensiunea inițială a cardului (First), apoi pe cele ale modalului (Last).

Apoi aplică un transform invers pe modal ca să îl facă să arate exact ca vechiul card (Invert) și, în final, animează acel transform către valorile reale ale modalului (Play). Totul se întâmplă pe GPU, deci obții 60 fps aproape garantat chiar și pe un telefon ieftin.

Capcana distorsionării textului și imaginilor

Cea mai frecventă problemă pe care am întâlnit-o la juniori când implementează asta e distorsiunea vizuală. Deoarece Framer Motion animează scaleX și scaleY pentru a potrivi dimensiunile, elementele copil (text, iconițe) vor arăta alungite sau turtite în timpul animației.

Am pățit-o pe un proiect mai vechi unde titlul cursului arăta ca tras de urechi timp de 300ms. Soluția curată este să pui layoutId doar pe elementele care își păstrează proporțiile (cum e containerul exterior sau imaginea) și să randezi conținutul text folosind o opacitate animată (opacity: 0 la 1), nu scalare.

Un trade-off sincer: Bundle size vs. Percepția vitezei

Framer Motion adaugă în jur de 30KB (gzipped) la bundle-ul tău de JS. Dacă ești pe un proiect unde fiecare kilobait contează extrem de mult și scorul de Lighthouse e principala ta obsesie, s-ar putea să te doară puțin.

Însă, din experiența mea, feedback-ul tactil și vizual pe care îl oferă o tranziție layoutId crește percepția de viteză a aplicației mai mult decât cele 50 de milisecunde pierdute la descărcarea bundle-ului. Utilizatorii simt că aplicația răspunde instantaneu.

Ce trebuie să eviți în producție

  1. NU pune layoutId pe o listă lungă cu sute de elemente randate simultan. Garbage collector-ul va gâfâi când măsoară DOM-ul pentru 100 de div-uri.
  2. Ai grijă la overflow: hidden pe containerele părinte. De multe ori animația va părea că se taie la jumătatea ecranului din cauza asta.
  3. Folosește întotdeauna AnimatePresence dacă vrei ca tranziția de închidere (unmount) să funcționeze la fel de lin ca cea de deschidere.

Voi ce folosiți pentru shared element transitions? Mergeți pe Framer Motion sau ați trecut la View Transitions API-ul nativ din browser?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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