SVG 转 JSX 实战:把图标做成 React 组件,别再让它渲染成空白
讲清 SVG 转 JSX 的每一处坑:属性驼峰化、class 转 className、空元素自闭合、内联 style 转对象,再带一段真实转换例子,以及内联 SVG 组件和直接用 img 的取舍。
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-width转strokeWidth、fill-rule转fillRule、stroke-linecap转strokeLinecap、clip-path转clipPath、stop-color转stopColor。 class转className,这是 JSX 里最广为人知的一条。- 内联
style字符串转对象。style="fill:red;stroke-width:2"要变成style={{ fill: 'red', strokeWidth: '2' }},注意对象里的键也得驼峰化。 - 空元素自闭合。
<path d="..."></path>里 SVG 元素允许结束标签,但单独的<br>这类在 JSX 里必须写成<br />。 - 注释改写。
<!-- 图标主体 -->在 JSX 里要写成{/* 图标主体 */}。
哪些大小写绝不能动
这是手工转换最容易翻车的地方。SVG 里有一批名字大小写敏感,改错了既不报错也不渲染。元素名 linearGradient、clipPath、feGaussianBlur 都得保留原样;属性名 viewBox、preserveAspectRatio、gradientUnits 也一样。
viewBox 那个大写的 B 尤其关键。我自己刚上手 React 那阵子,用编辑器全文小写化清理过一段 SVG,结果整个图标消失,排查了快二十分钟才发现是 viewbox 害的。所以驼峰化只能精准地作用在该改的属性上,不能一把梭把所有名字都小写。带命名空间的属性也要单独处理:冒号在 JSX 属性名里不合法,xlink:href 要改成 xlinkHref、xmlns:xlink 改成 xmlnsXlink、xml: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