Next.js中GSAP ScrollTrigger Pin属性水平滚动异常问题
Next.js中GSAP ScrollTrigger水平滚动异常问题
我在Next.js项目里用GSAP的ScrollTrigger组件的pin属性实现水平滚动,遇到了异常情况:从页面顶部向下滚动时,图片及所有内容会被隐藏;但从页面底部向上滚动时,水平滚动功能可以正常工作。
原代码
import { gsap } from "gsap"; import React, { useLayoutEffect } from "react"; const GallerySection = () => { useLayoutEffect(() => { const horizontal = document.querySelector("#container"); const totalWidth = horizontal.offsetWidth * 4 - window.innerWidth; console.log(totalWidth); const tl = gsap.timeline({ scrollTrigger: { trigger: "#container", markers: true, start: "top top", scrub: true, pin: true, pinSpacing: false, pinnedContainer: false, pinSpacer: false, }, }); tl.to("#wrapper", { x: -totalWidth }); // tl.kill(); () => tl.kill(); }, []); return ( <div id="container" className="h-[100vh] w-full"> <div id="wrapper" className="h-[100vh] w-full bg-green-500 flex justify-between items-center " > {/* 1 */} <div> <div className="h-[100vh] flex-col bg-green-500 w-[100vw] flex items-center justify-center relative"> <div className="w-[840px] h-[600px] " style={{ objectFit: "contain", }} > <img className="w-full h-full " style={{ objectFit: "fill", }} src="https://images.unsplash.com/photo-1566204773863-cf63e6d4ab88?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1345&q=100" alt="" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px] ">Dracaena Trifasciata</h1> <h2 className="text-[30px] ">Live the Beauty</h2> </div> </div> </div> {/* 2 */} <div> <div className="h-[100vh] flex-col bg-green-500 w-[100vw] flex items-center justify-center relative"> <div className="w-[840px] h-[600px] " style={{ objectFit: "contain", }} > <img className="w-full h-full " style={{ objectFit: "fill", }} src="https://images.unsplash.com/photo-1558603668-6570496b66f8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1300&q=100" alt="" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px] ">Cereus Penuvianus</h1> <h2 className="text-[30px] ">Live the Beauty</h2> </div> </div> </div> {/* 3 */} <div> <div className="h-[100vh] flex-col bg-green-500 w-[100vw] flex items-center justify-center relative"> <div className="w-[840px] h-[600px] " style={{ objectFit: "contain", }} > <img className="w-full h-full " style={{ objectFit: "fill", }} src="https://images.unsplash.com/photo-1567225557594-88d73e55f2cb?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=934&q=100" alt="" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px] ">Calliope</h1> <h2 className="text-[30px] ">Live the Beauty</h2> </div> </div> </div> {/* 4 */} <div> <div className="h-[100vh] flex-col bg-green-500 w-[100vw] flex items-center justify-center relative"> <div className="w-[840px] h-[600px] " style={{ objectFit: "contain", }} > <img className="w-full h-full " style={{ objectFit: "fill", }} src="https://images.unsplash.com/photo-1611145367651-6303b46e4040?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2006&q=100" alt="" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px] ">Golden Pothos</h1> <h2 className="text-[30px] ">Live the Beauty</h2> </div> </div> </div> </div> </div> ); }; export default GallerySection;
效果对比
- 从上到下滚动时内容隐藏:

- 从下到上滚动时正常显示:

问题修复方案
关键问题分析
- 错误的ScrollTrigger配置:
pinSpacer不是合法配置项,pinSpacing: false会导致没有足够的滚动空间,向下滚动时内容直接被挤出视口。 - 滚动距离计算错误:依赖
container的宽度计算总滚动距离不准确,应该直接取滚动容器的实际总宽度。 - 容器样式问题:
wrapper的justify-between会让子元素分散,且未设置flex-nowrap导致子元素换行,水平滚动逻辑失效。 - 清理函数未生效:原代码中
() => tl.kill()没有作为useLayoutEffect的返回值,组件卸载时无法清理GSAP实例,可能引发异常。
修复后的代码
import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import React, { useLayoutEffect } from "react"; // 必须注册ScrollTrigger插件 gsap.registerPlugin(ScrollTrigger); const GallerySection = () => { useLayoutEffect(() => { const container = document.querySelector("#container"); const wrapper = document.querySelector("#wrapper"); // 计算准确的滚动总距离:容器总宽度减去视口宽度 const totalScrollDistance = wrapper.scrollWidth - window.innerWidth; const tl = gsap.timeline({ scrollTrigger: { trigger: "#container", markers: true, start: "top top", // 定义滚动结束位置,保证有足够的滚动空间 end: `+=${totalScrollDistance}`, scrub: true, pin: true, // 让ScrollTrigger自动生成滚动占位空间 pinSpacing: true, }, }); tl.to(wrapper, { x: -totalScrollDistance }); // 组件卸载时清理动画和ScrollTrigger实例 return () => { tl.kill(); ScrollTrigger.getAll().forEach(trigger => trigger.kill()); }; }, []); return ( <div id="container" className="h-[100vh] w-full"> <div id="wrapper" className="h-[100vh] bg-green-500 flex flex-nowrap" // 让容器宽度自适应子元素总宽度 style={{ width: "fit-content" }} > {/* 1 */} <div className="w-[100vw] h-[100vh] flex flex-col bg-green-500 items-center justify-center relative"> <div className="w-[840px] h-[600px]"> <img className="w-full h-full object-fill" src="https://images.unsplash.com/photo-1566204773863-cf63e6d4ab88?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1345&q=100" alt="Dracaena Trifasciata" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px]">Dracaena Trifasciata</h1> <h2 className="text-[30px]">Live the Beauty</h2> </div> </div> {/* 2 */} <div className="w-[100vw] h-[100vh] flex flex-col bg-green-500 items-center justify-center relative"> <div className="w-[840px] h-[600px]"> <img className="w-full h-full object-fill" src="https://images.unsplash.com/photo-1558603668-6570496b66f8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1300&q=100" alt="Cereus Penuvianus" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px]">Cereus Penuvianus</h1> <h2 className="text-[30px]">Live the Beauty</h2> </div> </div> {/* 3 */} <div className="w-[100vw] h-[100vh] flex flex-col bg-green-500 items-center justify-center relative"> <div className="w-[840px] h-[600px]"> <img className="w-full h-full object-fill" src="https://images.unsplash.com/photo-1567225557594-88d73e55f2cb?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=934&q=100" alt="Calliope" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px]">Calliope</h1> <h2 className="text-[30px]">Live the Beauty</h2> </div> </div> {/* 4 */} <div className="w-[100vw] h-[100vh] flex flex-col bg-green-500 items-center justify-center relative"> <div className="w-[840px] h-[600px]"> <img className="w-full h-full object-fill" src="https://images.unsplash.com/photo-1611145367651-6303b46e4040?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2006&q=100" alt="Golden Pothos" /> </div> <div className="absolute left-[10%] bottom-[25%]"> <h1 className="text-[50px]">Golden Pothos</h1> <h2 className="text-[30px]">Live the Beauty</h2> </div> </div> </div> </div> ); }; export default GallerySection;
核心修复点
- 注册ScrollTrigger插件:GSAP 3.x版本需要手动注册该插件,否则
pin等功能无法正常工作。 - 修正ScrollTrigger配置:移除无效的
pinSpacer配置,开启pinSpacing: true并设置end属性,确保滚动区域有足够空间。 - 容器样式优化:给
wrapper添加flex-nowrap防止子元素换行,设置width: fit-content让容器宽度自适应子元素总宽度,移除justify-between避免布局混乱。 - 正确计算滚动距离:使用
wrapper.scrollWidth - window.innerWidth获取准确的滚动总距离,避免依赖父容器宽度的错误计算。 - 完善清理逻辑:在
useLayoutEffect中返回清理函数,组件卸载时销毁GSAP动画和ScrollTrigger实例,防止内存泄漏。
内容的提问来源于stack exchange,提问作者Venkatesh
相关产品推荐
相关产品推荐

