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

React中useMotionValueEvent实现滚动容器渐变遮罩效果失效问题求助

React中useMotionValueEvent实现滚动容器渐变遮罩效果失效问题求助

我最近刚接触React Motion(之前叫Framer Motion),正在学习基础用法,现在尝试实现文档里的「跟踪元素滚动偏移」效果,已经让scrollXprogress正常工作了,但遮罩渐变动画的部分一直没搞对——本来应该根据滚动到容器的开头、中间、结尾位置,分别显示不同的左右渐变遮罩,但现在遮罩只会停留在初始设置的样式上。

我看文档里的示例大多用TypeScript,而我用的是React JS,自己写了一段代码,但用maskImage.get()只能拿到初始值,滚动时不会更新。以下是我目前的代码:

import React, { useRef } from "react";
import {
  animate,
  motion,
  useMotionValue,
  useMotionValueEvent,
  useScroll,
} from "motion/react";

const ScrollXBox = () => {
  const left = `0%`;
  const right = `100%`;
  const leftInset = `20%`;
  const rightInset = `80%`;
  const transparent = `#0000`;
  const opaque = `#000`;

  function useScrollOverflowMask(scrollXProgress) {
    const maskImage = useMotionValue(
      `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})`
    );

    useMotionValueEvent(scrollXProgress, "change", (value) => {
      if (value === 0) {
        animate(
          maskImage,
          `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})`
        );
      } else if (value === 1) {
        animate(
          maskImage,
          `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${right}, ${opaque})`
        );
      } else if (
        scrollXProgress.getPrevious() === 0 ||
        scrollXProgress.getPrevious() === 1
      ) {
        animate(
          maskImage,
          `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${rightInset}, ${transparent})`
        );
      }
    });

    return maskImage;
  }

  const elements = ["A", "B", "C", "D"];
  const colors = [
    "bg-red-800",
    "bg-yellow-600",
    "bg-green-900",
    "bg-purple-900",
  ];

  const ref = useRef(null);
  const { scrollXProgress } = useScroll({ container: ref });
  const maskImage = useScrollOverflowMask(scrollXProgress);

  return (
    <div className="scroll-x h-screen flex flex-col justify-center items-center gap-4 bg-violet-400">
      <div id="example">
        <svg id="progress" width="80" height="80" viewBox="0 0 100 100">
          <circle
            cx="50"
            cy="50"
            r="40"
            pathLength="1"
            style={{ fill: "none", stroke: "red", strokeWidth: 20 }}
          />
          <motion.circle
            cx="50"
            cy="50"
            r="40"
            style={{
              fill: "none",
              stroke: "yellow",
              strokeWidth: 10,
              pathLength: scrollXProgress,
            }}
          />
        </svg>
        <div
          ref={ref}
          className="element-container overflow-x-scroll w-96 flex gap-4"
          style={{
            WebkitMaskImage: maskImage.get(),
            maskImage: maskImage.get(),
          }}
        >
          {elements.map((element, index) => (
            <motion.ul key={index} className="flex-shrink-0">
              <li>
                <div className={`${colors[index]} w-64 h-64`}></div>
              </li>
            </motion.ul>
          ))}
        </div>
      </div>
    </div>
  );
};

export default ScrollXBox;

问题分析与修复方案

我帮你排查了下,主要问题出在样式绑定方式和滚动进度的判断逻辑上:

  1. 你用maskImage.get()绑定样式,只会在组件初始渲染时获取一次值,后续maskImage更新时不会触发样式重新渲染;
  2. 精确判断value === 0或value === 1很难触发,因为滚动进度通常是接近0/1的小数,不会完全等于这两个值。

下面是修改后的代码,已经修复了这两个问题:

import React, { useRef } from "react";
import {
  animate,
  motion,
  useMotionValue,
  useMotionValueEvent,
  useScroll,
} from "motion/react";

const ScrollXBox = () => {
  // 定义遮罩相关常量
  const left = `0%`;
  const right = `100%`;
  const leftInset = `20%`;
  const rightInset = `80%`;
  const transparent = `#0000`;
  const opaque = `#000`;

  // 优化后的滚动遮罩钩子
  function useScrollOverflowMask(scrollXProgress) {
    const maskImage = useMotionValue(
      `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})`
    );

    useMotionValueEvent(scrollXProgress, "change", (value) => {
      const prevValue = scrollXProgress.getPrevious();
      // 接近滚动开头(进度小于0.01)
      if (value < 0.01) {
        animate(maskImage, `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})`);
      } 
      // 接近滚动结尾(进度大于0.99)
      else if (value > 0.99) {
        animate(maskImage, `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${right}, ${opaque})`);
      } 
      // 从边缘滚动到中间时切换遮罩
      else if (prevValue < 0.01 || prevValue > 0.99) {
        animate(maskImage, `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${rightInset}, ${transparent})`);
      }
    });

    return maskImage;
  }

  const elements = ["A", "B", "C", "D"];
  const colors = [
    "bg-red-800",
    "bg-yellow-600",
    "bg-green-900",
    "bg-purple-900",
  ];

  const ref = useRef(null);
  const { scrollXProgress } = useScroll({ container: ref });
  const maskImage = useScrollOverflowMask(scrollXProgress);

  return (
    <div className="scroll-x h-screen flex flex-col justify-center items-center gap-4 bg-violet-400">
      <div id="example">
        <svg id="progress" width="80" height="80" viewBox="0 0 100 100">
          <circle
            cx="50"
            cy="50"
            r="40"
            pathLength="1"
            style={{ fill: "none", stroke: "red", strokeWidth: 20 }}
          />
          <motion.circle
            cx="50"
            cy="50"
            r="40"
            style={{
              fill: "none",
              stroke: "yellow",
              strokeWidth: 10,
              pathLength: scrollXProgress,
            }}
          />
        </svg>
        {/* 换成motion.div,直接绑定maskImage,自动监听值更新 */}
        <motion.div
          ref={ref}
          className="element-container overflow-x-scroll w-96 flex gap-4"
          style={{
            WebkitMaskImage: maskImage,
            maskImage: maskImage,
          }}
        >
          {elements.map((element, index) => (
            <motion.ul key={index} className="flex-shrink-0">
              <li>
                <div className={`${colors[index]} w-64 h-64`}></div>
              </li>
            </motion.ul>
          ))}
        </motion.div>
      </div>
    </div>
  );
};

export default ScrollXBox;

关键修改说明:

  1. 把滚动容器换成motion.div,直接将maskImage作为样式属性传递,Motion会自动监听值的变化并更新样式,不用手动调用.get();
  2. 用范围判断value < 0.01/value > 0.99代替精确等于0/1,避免因为滚动进度是小数导致判断失效;
  3. 保留了从边缘滚动到中间时的遮罩切换逻辑,确保交互流畅。

你可以试试这个修改后的代码,滚动容器时遮罩应该会随着滚动位置正确切换渐变效果了。

备注:内容来源于stack exchange,提问作者TeDi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 19:29:31