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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 14:45:35