跳到主要内容

SVG 转 JSX 实战:把图标做成 React 组件,别再让它渲染成空白

讲清 SVG 转 JSX 的每一处坑:属性驼峰化、class 转 className、空元素自闭合、内联 style 转对象,再带一段真实转换例子,以及内联 SVG 组件和直接用 img 的取舍。

发布于 作者 李雷
#SVG #React #JSX #前端 #图标

SVG 转 JSX 实战:把图标做成 React 组件,别再让它渲染成空白

把一段 SVG 直接粘进 .tsx 文件,十有八九编译不过。第一个 stroke-width= 报错,内联的 style="..." 报错。就算你手动改完了,页面上可能还是一片空白,连个错误都没有。这篇文章把每一处坑摊开讲,顺便聊聊把图标做成组件、内联 SVG、以及和直接用 <img> 的区别。

为什么 SVG 不能直接当 JSX 用

JSX 不是 HTML,它会被编译成 React.createElement 调用。React 通过 DOM 上 SVG 元素的属性来设值,而这些属性的键是驼峰式的:它读的是 element.strokeWidth,不是 stroke-width。所以在 JSX 里直接写 stroke-width 是非法语法,那个连字符会被当成减号。即使你加了引号变成字符串,React 的 prop 处理也会忽略它。

需要改的不止一处。归纳下来有五类:

  • 表现属性驼峰化:stroke-widthstrokeWidthfill-rulefillRulestroke-linecapstrokeLinecapclip-pathclipPathstop-colorstopColor
  • classclassName,这是 JSX 里最广为人知的一条。
  • 内联 style 字符串转对象。style="fill:red;stroke-width:2" 要变成 style={{ fill: 'red', strokeWidth: '2' }},注意对象里的键也得驼峰化。
  • 空元素自闭合。<path d="..."></path> 里 SVG 元素允许结束标签,但单独的 <br> 这类在 JSX 里必须写成 <br />
  • 注释改写。<!-- 图标主体 --> 在 JSX 里要写成 {/* 图标主体 */}

哪些大小写绝不能动

这是手工转换最容易翻车的地方。SVG 里有一批名字大小写敏感,改错了既不报错也不渲染。元素名 linearGradientclipPathfeGaussianBlur 都得保留原样;属性名 viewBoxpreserveAspectRatiogradientUnits 也一样。

viewBox 那个大写的 B 尤其关键。我自己刚上手 React 那阵子,用编辑器全文小写化清理过一段 SVG,结果整个图标消失,排查了快二十分钟才发现是 viewbox 害的。所以驼峰化只能精准地作用在该改的属性上,不能一把梭把所有名字都小写。带命名空间的属性也要单独处理:冒号在 JSX 属性名里不合法,xlink:href 要改成 xlinkHrefxmlns:xlink 改成 xmlnsXlinkxml:space 改成 xmlSpace

一段真实的转换例子

输入这段从设计稿复制来的图标:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon"
     stroke="currentColor" stroke-width="2" fill="none">
  <path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16"/>
</svg>

勾上"包成组件",名字设成 MenuIcon,转出来是:

export default function MenuIcon(props) {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="icon"
         stroke="currentColor" strokeWidth={2} fill="none" {...props}>
      <path strokeLinecap="round" d="M4 6h16M4 12h16M4 18h16" />
    </svg>
  );
}

class 变了 className,stroke-width 变了 strokeWidth,stroke-linecap 变了 strokeLinecap,path 自闭合了,而 viewBox 的大写 B 原封不动。{...props} 排在内置属性之后,所以调用方传的东西会覆盖默认值。这一整套机械活,交给 SVG 转 JSX 工具 几秒钟就出结果,比逐个手改稳得多。

把图标做成组件:props 透传

包成组件的价值在 {...props} 这一行。它被展开到根 <svg> 上、排在内置属性之后,于是:

<MenuIcon className="size-5 text-red-500" width={32} onClick={open} />

class、尺寸、点击事件全落到了 SVG 上。再配合 stroke="currentColor"(大多数图标库都自带这一条),图标会自动继承周围文字的颜色。一个组件,任意颜色,常见情况下根本不用为每种配色各做一个。如果再勾上"移除 width/height",写死的尺寸就被去掉,交给 CSS 或 props 决定大小。

内联 SVG 和直接 img 的区别

为什么要费这个劲做成内联组件,而不是 <img src="icon.svg">?区别在控制力。

<img> 把 SVG 当成一张不透明的图片,你改不了它内部的颜色,currentColor 失效,CSS 选择器伸不进去,也没法绑事件。它适合那种"贴上去就好、永不联动"的插画。而内联 SVG 是真实的 DOM 节点,每个 <path> 都能被 CSS 命中、被 props 驱动、被 JS 操作。图标库基本都走内联这条路,正是因为要让一个字形适配深色模式、悬停态、各种尺寸。

代价是 SVG 的字节直接进了你的 JS 包,几十个内联图标会撑大首屏。所以选择很清楚:需要变色、变尺寸、响应交互的小图标走内联组件;大幅静态插画用 <img>,顺手用 SVG 优化工具 压一压再发,别让无用的元数据拖慢加载。

一个安全提醒

转换器只解析,不执行,用的是 DOMParser 的惰性文档,脚本不跑、外部引用不请求。但它不替你删 <script>on* 处理器:源 SVG 里带着的,会驼峰化后留进 JSX。来路不明的 SVG,转完务必审一遍再用。


Made by Toolora · Updated 2026-06-13