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

React中SVG动态下划线动画失效问题排查求助

滚动下划线动画的React TypeScript实现问题

原效果说明

我曾用JavaScript实现过滚动触发的下划线动画效果,基于Intersection Observer API,用生成的SVG实现下划线,核心原理如下:

  • 用<span>包裹需要加下划线的文本,方便定位
  • 将SVG直接嵌入HTML,放在目标文本的<span>内部,并给SVG添加squiggle类名用于CSS选择
  • 通过内联样式设置SVG的位置、宽高,确保下划线与文本位置匹配
  • 动画核心依赖SVG path的stroke-dasharray和stroke-dashoffset属性:初始时设置足够大的偏移量让描边隐藏;元素进入视口时触发underline动画显示下划线,移出时触发underline-out动画隐藏

React TypeScript + MUI实现问题

我尝试用React TypeScript结合MUI的styled组件重现该动画,计划后续添加Intersection Observer逻辑,但目前动画完全无法正常工作,即使使用内联样式也达不到预期效果。以下是我的App.tsx代码:

import Typography from '@mui/material/Typography';
import { Box, keyframes, styled } from '@mui/material';
import React from 'react';

const Underline = keyframes`
{
   "from": {
     strokeDashoffset: "1600",
   },
   "to": {
     strokeDashoffset: "200",
   }
 }
`;

const styles = {
   textDecoration: {
       textDecoration: 'none',
       cursor: 'pointer',
       color: 'inherit',
   },

   textEffect: {
       strokeDasharray: '1600',
       strokeDashoffset: '200',
       animationName: 'none',
   },

   squiggle: {
       position: 'absolute',
       top: '90%',
       left: '-9px',
   },

   'squiggle path': {
       stroke: '#FB9F18',
       strokeWidth: '14px',
       strokeLinecap: 'round',
       strokeDasharray: '1600',
       strokeDashoffset: '1600',

       animationName: 'underline-out',
       animationDuration: '1s',
       animationIterationCount: '1',
   },

   'textEffect.in-view .squiggle path': {
       animationName: 'underline',
       animationDuration: '1s',
       animationIterationCount: '1',
       animationFillMode: 'forwards',
   },

   '@keyframes underline': {
       from: {
           strokeDashoffset: '1600',
       },
       to: {
           strokeDashoffset: '200',
       },
   },

   '@keyframes underline-out': {
       from: {
           strokeDashoffset: '200',
       },
       to: {
           strokeDashoffset: '1600',
       },
   },
};


function UnderlineEffect() {

   return (
       <Box style={styles.textEffect}>
               <Typography>
                   Double your efficiency,{' '}
                   <span class="underline">
                       <a style={styles.textDecoration} href="www.somewebsite.com">
                           guaranteed.
                       </a>
                       <svg
                           width="215px"
                           height="25px"
                           style={styles.squiggle}
                           viewBox="0 0 466 29"
                           version="1.1"
                           xmlns="http://www.w3.org/2000/svg"
                           xmlns:xlink="http://www.w3.org/1999/xlink"
                           xml:space="preserve"
                           xmlns:serif="http://www.serif.com/"
                           style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
                           <g transform="matrix(1,0,0,1,-4055,-7503)">
                               <g transform="matrix(1,0,0,1,2942.74,0)">
                                   <g
                                       id="squiggle-underline"
                                       transform="matrix(0.59639,0.0106812,-0.0121867,0.680453,651.002,2140.64)">
                                       <path
                                           d="M976.992,7882.9C1019.57,7882.9 1065.7,7874.34 1108.59,7871.65C1246.32,7863.04 1383.5,7857.74 1521.52,7857.74C1541.4,7857.74 1709,7857.97 1714.47,7868.91C1715.77,7871.51 1710.77,7870.69 1710.29,7870.73C1695.62,7871.88 1681.88,7871.57 1667.06,7871.96C1646.09,7872.51 1625.12,7872.92 1604.15,7873.36C1564.57,7874.19 1525.13,7876.05 1485.69,7879.23C1411.5,7885.22 1337.41,7889.35 1263.06,7892.45C1196.87,7895.22 1130.52,7894 1064.4,7897.12C1021.44,7899.14 978.37,7907.57 935.437,7907.57"
                                           style="fill:none;"
                                       />
                                   </g>
                               </g>
                           </g>
                       </svg>
                   </span>
               </Typography>
       </Box>
   );
}

export { UnderlineEffect };

问题分析与修复方案

核心问题点

  1. 内联样式不支持嵌套选择器与keyframes:MUI的内联样式对象无法解析squiggle path这类嵌套选择器,也不能直接定义@keyframes
  2. keyframes语法错误:原Underline关键帧的写法不符合MUIkeyframes函数的要求
  3. 定位上下文缺失:SVG用了绝对定位,但父元素没有设置position: relative,导致SVG相对于页面而非文本容器定位
  4. React类名使用错误:JSX中应该用className而非class

修复后的代码示例

import Typography from '@mui/material/Typography';
import { Box, keyframes, styled } from '@mui/material';
import React from 'react';

// 定义动画关键帧
const underlineIn = keyframes`
  from {
    stroke-dashoffset: 1600;
  }
  to {
    stroke-dashoffset: 200;
  }
`;

const underlineOut = keyframes`
  from {
    stroke-dashoffset: 200;
  }
  to {
    stroke-dashoffset: 1600;
  }
`;

// 定义Styled组件
const TextContainer = styled(Box)({
  position: 'relative',
});

const SquiggleSVG = styled('svg')(({ theme }) => ({
  position: 'absolute',
  top: '90%',
  left: '-9px',
  '& path': {
    stroke: '#FB9F18',
    strokeWidth: '14px',
    strokeLinecap: 'round',
    strokeDasharray: '1600',
    strokeDashoffset: '1600',
    animation: `${underlineOut} 1s 1`,
  },
  '&.in-view path': {
    animation: `${underlineIn} 1s 1 forwards`,
  },
}));

const StyledLink = styled('a')({
  textDecoration: 'none',
  cursor: 'pointer',
  color: 'inherit',
});

function UnderlineEffect() {
  // 后续可结合Intersection Observer修改isInView状态
  const [isInView, setIsInView] = React.useState(false);

  return (
    <Box>
      <Typography>
        Double your efficiency,{' '}
        <TextContainer>
          <StyledLink href="www.somewebsite.com">
            guaranteed.
          </StyledLink>
          <SquiggleSVG
            className={isInView ? 'in-view' : ''}
            width="215px"
            height="25px"
            viewBox="0 0 466 29"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            xmlnsXlink="http://www.w3.org/1999/xlink"
            xmlSpace="preserve"
            style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
          >
            <g transform="matrix(1,0,0,1,-4055,-7503)">
              <g transform="matrix(1,0,0,1,2942.74,0)">
                <g
                  id="squiggle-underline"
                  transform="matrix(0.59639,0.0106812,-0.0121867,0.680453,651.002,2140.64)"
                >
                  <path
                    d="M976.992,7882.9C1019.57,7882.9 1065.7,7874.34 1108.59,7871.65C1246.32,7863.04 1383.5,7857.74 1521.52,7857.74C1541.4,7857.74 1709,7857.97 1714.47,7868.91C1715.77,7871.51 1710.77,7870.69 1710.29,7870.73C1695.62,7871.88 1681.88,7871.57 1667.06,7871.96C1646.09,7872.51 1625.12,7872.92 1604.15,7873.36C1564.57,7874.19 1525.13,7876.05 1485.69,7879.23C1411.5,7885.22 1337.41,7889.35 1263.06,7892.45C1196.87,7895.22 1130.52,7894 1064.4,7897.12C1021.44,7899.14 978.37,7907.57 935.437,7907.57"
                    style={{ fill: 'none' }}
                  />
                </g>
              </g>
            </g>
          </SquiggleSVG>
        </TextContainer>
      </Typography>
    </Box>
  );
}

export { UnderlineEffect };

修复说明

  • 用MUI的styled组件替代内联样式,支持嵌套选择器和动画绑定
  • 正确使用keyframes函数定义符合CSS标准的动画
  • 给文本容器添加position: relative,确保SVG绝对定位相对于文本
  • 将class替换为React标准的className
  • 预留isInView状态,后续可结合Intersection Observer API实现滚动触发逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 09:15:42