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

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;

效果对比

  • 从上到下滚动时内容隐藏:从上到下滚动内容隐藏
  • 从下到上滚动时正常显示:从下到上滚动正常显示

问题修复方案

关键问题分析

  1. 错误的ScrollTrigger配置:pinSpacer不是合法配置项,pinSpacing: false会导致没有足够的滚动空间,向下滚动时内容直接被挤出视口。
  2. 滚动距离计算错误:依赖container的宽度计算总滚动距离不准确,应该直接取滚动容器的实际总宽度。
  3. 容器样式问题:wrapper的justify-between会让子元素分散,且未设置flex-nowrap导致子元素换行,水平滚动逻辑失效。
  4. 清理函数未生效:原代码中() => 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;

核心修复点

  1. 注册ScrollTrigger插件:GSAP 3.x版本需要手动注册该插件,否则pin等功能无法正常工作。
  2. 修正ScrollTrigger配置:移除无效的pinSpacer配置,开启pinSpacing: true并设置end属性,确保滚动区域有足够空间。
  3. 容器样式优化:给wrapper添加flex-nowrap防止子元素换行,设置width: fit-content让容器宽度自适应子元素总宽度,移除justify-between避免布局混乱。
  4. 正确计算滚动距离:使用wrapper.scrollWidth - window.innerWidth获取准确的滚动总距离,避免依赖父容器宽度的错误计算。
  5. 完善清理逻辑:在useLayoutEffect中返回清理函数,组件卸载时销毁GSAP动画和ScrollTrigger实例,防止内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 07:29:52