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 };
问题分析与修复方案
核心问题点
- 内联样式不支持嵌套选择器与keyframes:MUI的内联样式对象无法解析
squiggle path这类嵌套选择器,也不能直接定义@keyframes - keyframes语法错误:原
Underline关键帧的写法不符合MUIkeyframes函数的要求 - 定位上下文缺失:SVG用了绝对定位,但父元素没有设置
position: relative,导致SVG相对于页面而非文本容器定位 - 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
相关产品推荐
相关产品推荐

