You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Framer Motion动画在map渲染的组件中失效的解决方法

Framer Motion 卡片动画封装组件后失效的修复方案

问题背景

单卡片实现时,Framer Motion的布局动画(layout)和切换动画正常运行,但将卡片封装为FeaturedProject组件并通过map遍历数据渲染后,动画完全失效,包括卡片展开时的布局变化、图片切换动画都无法正常触发。尝试过相关Stack Overflow方案但未解决。

核心原因

  1. 列表中每个卡片的layout动画需要唯一标识来追踪元素,未设置layoutId导致Framer Motion无法识别布局变化
  2. 封装组件后,状态管理未独立(原单卡片用父组件状态,现在每个卡片需自己维护展开状态)
  3. AnimatePresence的上下文和组件挂载逻辑不匹配,导致退出/进入动画无法触发

修复步骤

1. 给组件添加独立状态与唯一layoutId

每个FeaturedProject组件需要自己维护cardOpen状态,同时给卡片元素设置基于项目唯一标识的layoutId:

// FeaturedProject.jsx
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

export default function FeaturedProject({ projectName, period, techStack, description, coverImg, bgImg }) {
  const [cardOpen, setCardOpen] = useState(false);

  return (
    <motion.div
      layoutId={`project-card-${projectName}`}
      style={cardOpen && { gridColumn: "1 / -1" }}
      className={`card group relative w-full h-72 bg-primary rounded-lg overflow-hidden ${cardOpen ? "flex gap-5 p-10 shadow-inner" : "cursor-pointer"}`}
      onClick={() => {
        if (cardOpen) return;
        setCardOpen(true);
      }}
    >
      {/* 图片切换动画部分 */}
      <AnimatePresence mode="wait">
        {cardOpen ? (
          <motion.div
            className="h-full absolute -bottom-32 -left-4"
            initial={{ y: -100 }}
            animate={{ y: 0 }}
            exit={{ y: -100 }}
          >
            <motion.img
              className="h-full z-0"
              src={bgImg}
              alt={`${projectName} background`}
            />
          </motion.div>
        ) : (
          <motion.img
            transition={{ ease: "easeIn", duration: 0.1 }}
            initial={{ x: -500 }}
            animate={{ x: 0 }}
            exit={{ opacity: 0 }}
            className="absolute top-0 left-0 rounded-lg w-full h-full object-cover brightness-50 group-hover:scale-110 group-hover:brightness-100"
            src={coverImg}
            alt={`${projectName} cover`}
          />
        )}
      </AnimatePresence>

      {/* 内容区域 */}
      <motion.div 
        className={`text-white z-10 w-full ${cardOpen ? "" : "absolute bottom-3 left-3 group-hover:bottom-5"}`}
        layout="position"
      >
        <motion.div layout="position">
          {cardOpen && <span className="montserrat font-light text-xs">{period}</span>}
          <div className="flex justify-between">
            <h1 className="text-4xl font-bold">{projectName}</h1>
            {cardOpen && (
              <button 
                className="cursor-pointer" 
                onClick={(e) => {
                  e.stopPropagation();
                  setCardOpen(false);
                }}
              >
                X
              </button>
            )}
          </div>
          <div className="flex gap-2 text-xs">
            {techStack.map(tech => (
              <span key={tech} className="border rounded-full py-px px-2">{tech}</span>
            ))}
          </div>
        </motion.div>
        {cardOpen && (
          <motion.div 
            className="mt-2" 
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ duration: 1, ease: "easeIn" }}
          >
            <p className="montserrat">{description}</p>
          </motion.div>
        )}
      </motion.div>
    </motion.div>
  );
}

2. 父级Grid容器添加统一layout属性

父容器需要保留layout属性,确保列表布局变化时能被Framer Motion追踪:

// 父组件
import { motion } from "framer-motion";
import FeaturedProject from "./FeaturedProject";

export default function Projects() {
  const featuredProjects = [
    // 你的项目数据,包含projectName、period、techStack等字段
  ];

  return (
    <motion.div 
      layout
      className="grid grid-cols-2 md:grid-cols-3 gap-8 py-10"
    >
      {featuredProjects.map(project => (
        <FeaturedProject key={project.projectName} {...project} />
      ))}
    </motion.div>
  );
}

3. 关键细节修正

  • 给关闭按钮添加e.stopPropagation(),避免点击关闭时触发卡片的onClick事件
  • 调整AnimatePresence内的条件判断为cardOpen ? ... : ...,确保退出动画能正确触发
  • 给所有动态生成的元素(比如techStack的标签)添加唯一key
  • 明确设置initial动画属性,确保组件挂载时的入场动画正常执行

为什么之前的方案无效?

之前参考的方案可能只解决了列表项增删时的动画问题,但你的场景是单个列表项的布局扩张动画,需要通过layoutId明确标记子元素的唯一身份,同时保证每个组件的状态独立,才能让Framer Motion正确计算布局变化并生成动画。

内容的提问来源于stack exchange,提问作者Brian Harianja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 18:20:34