Framer Motion打开菜单时whileInView标题动画意外重置问题
问题原因
- 核心是对
viewport={{ once: true }}的作用存在理解偏差。这个配置只会让「元素进入视口时触发动画」的逻辑执行1次,不会锁定动画结束后的最终状态。whileInView本身的逻辑是实时和元素的视口可见性绑定的:只要元素因为任何原因(弹出全屏菜单抽屉、布局偏移、父级添加transform属性)被判定为不在视口内,就会立刻回退到initial定义的hidden状态。你打开的菜单抽屉一般是fixed定位全屏覆盖,会直接导致原有页面的标题元素被判定为不可见,这就是触发重置的直接原因。 - 额外的写法问题:你给每个单字母的
<motion.span>都单独加了viewport检测配置,打开抽屉时所有字母会同时判定为不可见,集体回到translateY(100%)的初始状态;外层包裹的AnimatePresence在这里完全没有作用,反而会增加不必要的重渲染判定。
修复方案
不要依赖whileInView做一次性入场动画,改用useInView钩子手动控制动画状态,一旦元素进入过视口就永久锁定为动画完成状态,不会因为后续视口变化重置。
修改后的完整代码如下:
import { motion, useInView } from "framer-motion"; import { useRef } from "react"; function AnimatedHeading(props) { const { text, stagger = 0.015, delay = 0.5 } = props || {} // 绑定元素ref,检测进入视口状态,once保证只触发一次 const headingRef = useRef(null); const isInView = useInView(headingRef, { once: true, margin: "0px 0px -100px 0px" // 可调整触发阈值,负值表示提前触发 }); // 单词包裹组件,防止单词内部断行 const WordWrap = (props) => { return <span className="whitespace-nowrap">{props.children}</span>; } // 拆分文本为单词、单字母数组 const splitWords = text.split(" "); const words = []; for (const [, item] of splitWords.entries()) { words.push(item.split("")); } // 每个单词末尾追加不换行空格 words.map((word) => { return word.push("\u00A0"); }); const variants = { hidden: { y: '100%', transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.85 } }, animate: { y: '0%', transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.75 } } }; return ( <> <span className="sr-only">{text}</span> <motion.span ref={headingRef} className="inline-block" initial="hidden" // 手动绑定动画状态,进入过视口就永久保持完成态 animate={isInView ? "animate" : "hidden"} transition={{ delayChildren: delay, staggerChildren: stagger }} > {words.map((word, n) => ( <WordWrap key={n}> {word.flat().map((char, index) => ( <span key={index} className="overflow-hidden pb-1 inline-block"> <motion.span key={`animated-heading-${char}-${index}`} variants={variants} className="relative inline-block overflow-visible" > {char} </motion.span> </span> ))} </WordWrap> ))} </motion.span> </> ) }
关键修改点
- 移除所有
whileInView属性和子元素多余的viewport配置,删掉无用的AnimatePresence包裹 - 用
useRef绑定标题容器,通过useInView检测元素可见性,once: true参数会在元素第一次进入视口后永久把isInView锁定为true,后续不管打开菜单还是布局偏移都不会改变状态 - 改用
animate属性手动控制动画状态,只要isInView为true就一直保持animate的最终样式,彻底解决重置问题 useInView的margin参数可以调整动画触发的视口阈值,示例里的-100px表示元素距离视口底部还有100px时就触发动画,可根据实际需求调整。
内容的提问来源于stack exchange,提问作者Jay
相关产品推荐
相关产品推荐

