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

Framer Motion布局过渡导致兄弟元素异常问题求助

食谱卡片应用中Framer Motion标题弹跳问题解决

问题描述

刚学习React和Framer Motion,开发食谱卡片应用时遇到以下问题:打开第2张及之后的卡片(下称SUB卡片)并关闭后,再打开任意靠前的卡片,之前打开过又关闭的SUB卡片的{recipe.title}标题会脱离卡片背景,跟着靠前卡片的缩放动作弹跳。

相关代码

RecipeTable.jsx 组件

export default function RecipeTable({ recipes }) {
    return (
        <div className="container-fluid">
                <div className="row">
                    <LayoutGroup>
                        {recipes.map((recipe, index) => (
                            <RecipeCard
                                recipe={recipe}
                                key={index}
                            />
                        ))}
                    </LayoutGroup>
                </div>
        </div>
    );
}

RecipeCard.jsx 组件

export default function RecipeCard({ recipe, key }) {
    const [isOpen, setIsOpen] = useState(false);
    const controls = useAnimation();

    useEffect(() => {
        if (isOpen) {
            controls.start({
                scale: [1, 1.2, 1,],
                rotate: [0, 270, 0],
                transition: { duration: 0.7 },
            });
        }
    }, [isOpen, controls]);

    return (
        <motion.div className="card-container">
            <motion.div
                transition={{ layout: { type: "spring", duration: 0.7 }}}
                layout
                onClick={() => ! isOpen && setIsOpen(true)}
                className="card"
                style={isOpen ? {} : {cursor: "pointer"}}
                whileHover={ isOpen ? {} : { scale: 1.05 }}
            >
                { isOpen && (
                    <motion.button
                        className="close-button"
                        onClick={() => setIsOpen(false)}
                        initial={{scale: 1, rotate: 0}}
                        animate={controls}
                        whileHover={{scale: 1.3, rotate: 0}}
                    >
                        &times;
                    </motion.button>
                )}
                <motion.h2 layout="position">
                    {recipe.title}
                </motion.h2>
                {isOpen && (
                    <motion.div
                        className="card-expand"
                        initial={{opacity: 0}}
                        animate={{opacity: 1}}
                        transition={{duration: 0.5}}
                    >
                        <p>{recipe.ingredients}</p>
                        <p>{recipe.instructions}</p>
                    </motion.div>
                )}
            </motion.div>
        </motion.div>
    )
}

my.css 样式文件

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    font-family: sans-serif;
}

body {
    background: #ADA996;  /* fallback for old browsers */
    background: -webkit-linear-gradient(to right, #EAEAEA, #DBDBDB, #F2F2F2, #ADA996);  /* Chrome 10-25, Safari 5.1-6 */
    background: linear-gradient(to right, #EAEAEA, #DBDBDB, #F2F2F2, #ADA996); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}

.card-container {
    display: flex;
    justify-content: center;
    margin-top: 2rem;
    margin-bottom: 2rem;
}

.card {
    background: #fff;
    padding: 3rem 5rem;
    border-radius: 1rem;
}

.card p {
    line-height: 150%;
}

.close-button {
    position: absolute;
    top: -1rem;
    right: -1rem;
    border: none;
    background-color: black;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    color: #fff;
    font-size: 20px;
}

.card-expand {
    max-width: 60vw;
}

@media (max-width: 768px) {
    .card-expand {
        max-width: 100vw;
    }
}

已尝试的解决方案

  • 给外层<motion.div>设置layout属性,修复了标题弹跳,但破坏了其他卡片为打开卡片腾空间的动画效果。
  • 给标题添加空过渡transition={{}}后略有改善,但标题仍会在卡片内晃动,且卡片打开动画不再流畅:
<motion.h2 layout="position" transition={{}}>
    {recipe.title}
</motion.h2>

问题根源与修复方案

问题根源

标题弹跳是因为layout="position"让标题在LayoutGroup的布局计算中保留了之前打开状态的位置缓存,当其他卡片触发布局变化时,错误触发了标题的位置动画;同时卡片容器的layout和标题的layout="position"之间存在动画冲突,导致异常行为。

修复步骤

  1. 移除标题的layout="position"
    让标题作为卡片容器的子元素,继承容器的布局变化即可。若需要标题的展开动画,可基于isOpen状态设置显式动画,而非依赖layout:
<motion.h2
    initial={{ y: 0 }}
    transition={{ duration: 0.3 }}
>
    {recipe.title}
</motion.h2>
  1. 给卡片添加相对定位
    关闭按钮使用了position: absolute,父元素需要设置相对定位才能正确放置按钮,同时避免布局偏移:
.card {
    background: #fff;
    padding: 3rem 5rem;
    border-radius: 1rem;
    position: relative; /* 添加此行 */
}
  1. 替换列表key为唯一标识
    避免用index作为key,改用recipe.id这类稳定唯一值,防止列表更新时的key冲突导致布局异常:
{recipes.map((recipe) => (
    <RecipeCard
        recipe={recipe}
        key={recipe.id} // 改用recipe的唯一id
    />
))}
  1. 简化关闭按钮动画逻辑
    直接用animate属性绑定状态,无需单独使用useAnimation:
{isOpen && (
    <motion.button
        className="close-button"
        onClick={() => setIsOpen(false)}
        initial={{ scale: 1, rotate: 0 }}
        animate={{ scale: [1, 1.2, 1], rotate: [0, 270, 0] }}
        transition={{ duration: 0.7 }}
        whileHover={{ scale: 1.3, rotate: 0 }}
    >
        &times;
    </motion.button>
)}

完整修改后的RecipeCard.jsx

export default function RecipeCard({ recipe }) {
    const [isOpen, setIsOpen] = useState(false);

    return (
        <motion.div className="card-container">
            <motion.div
                transition={{ layout: { type: "spring", duration: 0.7 }}}
                layout
                onClick={() => !isOpen && setIsOpen(true)}
                className="card"
                style={isOpen ? {} : { cursor: "pointer" }}
                whileHover={!isOpen ? { scale: 1.05 } : {}}
            >
                {isOpen && (
                    <motion.button
                        className="close-button"
                        onClick={() => setIsOpen(false)}
                        initial={{ scale: 1, rotate: 0 }}
                        animate={{ 
                            scale: [1, 1.2, 1], 
                            rotate: [0, 270, 0] 
                        }}
                        transition={{ duration: 0.7 }}
                        whileHover={{ scale: 1.3, rotate: 0 }}
                    >
                        &times;
                    </motion.button>
                )}
                <motion.h2
                    initial={{ opacity: 1 }}
                    transition={{ duration: 0.3 }}
                >
                    {recipe.title}
                </motion.h2>
                {isOpen && (
                    <motion.div
                        className="card-expand"
                        initial={{ opacity: 0, height: 0 }}
                        animate={{ opacity: 1, height: "auto" }}
                        exit={{ opacity: 0, height: 0 }}
                        transition={{ duration: 0.5 }}
                    >
                        <p>{recipe.ingredients}</p>
                        <p>{recipe.instructions}</p>
                    </motion.div>
                )}
            </motion.div>
        </motion.div>
    )
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 09:44:55