Framer Motion动画在map渲染的组件中失效的解决方法
Framer Motion 卡片动画封装组件后失效的修复方案
问题背景
单卡片实现时,Framer Motion的布局动画(layout)和切换动画正常运行,但将卡片封装为FeaturedProject组件并通过map遍历数据渲染后,动画完全失效,包括卡片展开时的布局变化、图片切换动画都无法正常触发。尝试过相关Stack Overflow方案但未解决。
核心原因
- 列表中每个卡片的
layout动画需要唯一标识来追踪元素,未设置layoutId导致Framer Motion无法识别布局变化 - 封装组件后,状态管理未独立(原单卡片用父组件状态,现在每个卡片需自己维护展开状态)
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
相关产品推荐
相关产品推荐

