使用framer-motion实现动画完成前禁止滚动到下一个组件
解决framer-motion星空动画滚动拦截问题
核心思路是先拦截滚动直到动画完成,再允许页面滚动到后续区域,以下是具体实现方案:
1. 核心状态与滚动监听实现
用React状态控制动画完成状态,结合framer-motion的useScroll获取滚动进度,同时监听原生滚动事件做拦截:
import { useState, useEffect } from 'react'; import { motion, useScroll, useTransform } from 'framer-motion'; export default function StarryHero() { const [isAnimationComplete, setIsAnimationComplete] = useState(false); const { scrollYProgress } = useScroll(); // 关联滚动进度到动画参数 const starSpeed = useTransform(scrollYProgress, [0, 1], [5, 30]); const textScale = useTransform(scrollYProgress, [0, 1], [1, 2]); const textOpacity = useTransform(scrollYProgress, [0, 0.8, 1], [1, 0, 0]); // 监听滚动进度,判断动画是否完成 useEffect(() => { const unsubscribe = scrollYProgress.onChange((progress) => { if (progress >= 1) { setIsAnimationComplete(true); } }); return unsubscribe; }, [scrollYProgress]); // 拦截滚动事件 useEffect(() => { const handleScroll = (e) => { if (!isAnimationComplete) { e.preventDefault(); // 手动控制滚动步长,优化平滑度 const delta = e.deltaY || (e.touches && e.touches[0].clientY - e.targetTouches[0].clientY); if (delta > 0) { window.scrollBy(0, window.innerHeight * 0.02); } } }; if (!isAnimationComplete) { window.addEventListener('wheel', handleScroll, { passive: false }); window.addEventListener('touchmove', handleScroll, { passive: false }); } else { window.removeEventListener('wheel', handleScroll); window.removeEventListener('touchmove', handleScroll); } return () => { window.removeEventListener('wheel', handleScroll); window.removeEventListener('touchmove', handleScroll); }; }, [isAnimationComplete]); return ( <> {/* 星空英雄区,占满视口 */} <motion.div style={{ height: '100vh', position: 'relative', overflow: 'hidden', backgroundColor: '#0a0e27' }} > {/* 星空背景(示例,可替换成你的星空组件) */} <motion.div style={{ position: 'absolute', inset: 0, backgroundImage: 'radial-gradient(white 1px, transparent 1px)', backgroundSize: '20px 20px', animation: `starMove ${starSpeed}s linear infinite`, }} /> {/* 上方文字 */} <motion.h1 style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', color: 'white', scale: textScale, opacity: textOpacity, fontSize: '3rem' }} > Welcome to Repl.it </motion.h1> </motion.div> {/* 后续内容区域 */} <div style={{ height: '100vh', backgroundColor: '#fff', padding: '2rem' }}> <h2>后续页面内容</h2> <p>动画完成后才能滚动到这里</p> </div> </> ); } // 星空滚动动画的CSS const style = document.createElement('style'); style.textContent = ` @keyframes starMove { from { backgroundPosition: 0 0; } to { backgroundPosition: 0 100vh; } } `; document.head.appendChild(style);
2. 关键细节说明
- 滚动拦截逻辑:通过
wheel和touchmove事件拦截滚动,passive: false是必须的,否则无法阻止默认行为。动画完成后移除监听,恢复正常滚动。 - 动画进度关联:用
useScroll的scrollYProgress(0到1的范围)控制星空速度、文字缩放和透明度,当进度到1时标记动画完成。 - 布局控制:英雄区设置
height: 100vh,确保初始占满视口,后续内容在其下方,这样动画完成后滚动自然进入下一个区域。 - 平滑滚动优化:拦截时手动控制滚动步长(
window.scrollBy),避免生硬的滚动体验。
3. 简洁替代方案:用framer-motion控制容器滚动
如果觉得原生事件拦截麻烦,也可以通过控制容器的overflow属性实现,逻辑更简洁:
import { useState, useEffect } from 'react'; import { motion, useScroll, useTransform } from 'framer-motion'; export default function StarryHero() { const { scrollYProgress } = useScroll({ target: document.documentElement, offset: ["start start", "end start"] }); const [isAnimationComplete, setIsAnimationComplete] = useState(false); useEffect(() => { const unsubscribe = scrollYProgress.onChange((progress) => { progress >= 1 && setIsAnimationComplete(true); }); return unsubscribe; }, [scrollYProgress]); // 关联滚动进度到动画参数 const starSpeed = useTransform(scrollYProgress, [0, 1], [5, 30]); const textScale = useTransform(scrollYProgress, [0, 1], [1, 2]); const textOpacity = useTransform(scrollYProgress, [0, 0.8, 1], [1, 0, 0]); return ( <div style={{ overflow: isAnimationComplete ? 'auto' : 'hidden', height: '100vh', backgroundColor: '#0a0e27' }}> <motion.div style={{ height: '200vh' }}> {/* 星空和文字动画区域 */} <motion.div style={{ height: '100vh', position: 'sticky', top: 0 }}> <motion.div style={{ position: 'absolute', inset: 0, backgroundImage: 'radial-gradient(white 1px, transparent 1px)', backgroundSize: '20px 20px', animation: `starMove ${starSpeed}s linear infinite`, }} /> <motion.h1 style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', color: 'white', scale: textScale, opacity: textOpacity, fontSize: '3rem' }} > Welcome to Repl.it </motion.h1> </motion.div> {/* 后续内容 */} <div style={{ height: '100vh', backgroundColor: '#fff', padding: '2rem' }}> <h2>后续页面内容</h2> <p>动画完成后才能滚动到这里</p> </div> </motion.div> </div> ); } // 星空滚动动画的CSS const style = document.createElement('style'); style.textContent = ` @keyframes starMove { from { backgroundPosition: 0 0; } to { backgroundPosition: 0 100vh; } } `; document.head.appendChild(style);
这个方案通过外层容器的overflow属性控制滚动,动画完成后才允许容器滚动,无需手动监听原生事件,适合布局结构简单的场景。
内容的提问来源于stack exchange,提问作者user2794212
相关产品推荐
相关产品推荐

