Bootstrap Icons 1.11.3 图标接入、子集化与构建实践

发布时间:2026/10/30 8:38:36
Bootstrap Icons 1.11.3 图标接入、子集化与构建实践

简介:Bootstrap Icons 1.11.3 是一套面向网页与前端界面开发者的开源 SVG 图标合集,可独立于 Bootstrap 框架使用,主要解决项目里图标资源分散、风格不统一、后期替换成本高的问题,适合前端工程师、UI 设计人员及需要快速搭建原型的初学者直接调用。压缩包共 2000 个文件,主体是 1994 个 svg 矢量图标,另含 2 个 css 样式表、1 个 scss 源文件、1 个 json 图标索引,以及 2 个 woff 与 woff2 字体文件,整体约 1.58MB,体积轻量,便于随项目一起打包分发。图标覆盖箭头、标签、搜索等基础控件和社交品牌标识,命名遵循语义化约定,既可用类名调用,也可内联 SVG;配套的 CSS 与字体文件支持传统页面快速接入,json 与 scss 则方便在 Vue、React、Angular 等构建流程中按需筛选与二次定制。矢量格式缩放不失真,颜色和尺寸可由 CSS 控制,有助于保持界面风格统一。已有 440 人学习下载,可作为中小型项目与设计规范落地的图标底座。

1. 一个压缩包顶掉三套图标源:bootstrap-icons-1.11.3 到底装了什么

后台项目做久了的开发者基本都遇到过这种情况:图标从设计稿导出一批、从旧项目拷贝一批、临时又从图标站下一批,结果同样是「设置」,一套描边 1.5px、一套 2px、一套圆角,放在同一行菜单里肉眼可见地不协调。bootstrap-icons-1.11.3.zip 这个包解决的正是这个问题:一套统一 16×16 网格、纯填充路径、kebab-case 命名的图标资产,同一份图形同时以单枚 SVG、字体文件、SVG sprite、SCSS 源码四种形态分发。它不抢戏,风格偏中性,覆盖表单、表格、导航、状态提示、文件类型这些后台高频场景。适合正在做管理后台、内部工具、文档站并且不想为图标再引一个重型组件库的人;不适合需要彩色插画、品牌吉祥物、手绘风格的营销落地页。下面按「拆包看清有什么 → 三条接入路线怎么选 → 工程化裁剪 → 排查 → 版本管理」的顺序,把这包东西落到你自己的构建链里。

2. 拆包先认清目录:四种分发形态对应四类工程约束

很多人解压之后第一反应是找 CSS 文件,把bootstrap-icons.css往<head>一塞就完事。能跑,但这一步跳过了选型,后面九成的翻车都源于此:字体图标在深色模式下颜色被!important顶掉、sprite 在file://下不显示、内联 SVG 让打包体积翻倍。先把包里到底有什么看明白,再决定用哪一份。

2.1 解压后你会看到的五块内容

1.11.3 这个版本的分发结构和之前的 1.10、1.9 基本一致,解压后大致是这样几块:

路径内容谁在用
icons/两千多个单枚.svg,文件名就是图标名需要单独拷贝、二次编辑、当静态资源存的对象存储
font/fonts/bootstrap-icons.woff2、bootstrap-icons.woff字体图标方案实际加载的两个二进制
font/bootstrap-icons.css@font-face定义 + 每个图标的::before码点规则直接<link>引入或用打包器 import
font/bootstrap-icons.scss同样的规则,但字体目录、字体族名抽成了变量走 Sass 构建、需要改名或改路径的项目
font/bootstrap-icons.json图标名到十六进制码点的映射表子集化裁剪、版本 diff、自建图标选择器
根目录bootstrap-icons.svg所有图标汇成一个 sprite,每个图标一个<symbol id="图标名">想用<use>引用又不想加载字体的项目

第一次打开bootstrap-icons.css时,建议重点扫一遍@font-face那一行。你会看到src里的 url 后面挂了一串类似?2820a26e...的查询串,这是官方用来做缓存击穿的 hash。这个细节在后面改字体路径时很容易变成坑:有人把字体拷到自己目录、把 url 改成相对路径,顺手把查询串删了,结果 CDN 上旧版本字体一直命中缓存,页面显示的是上一个版本的码点,图标全错位。

bootstrap-icons.json是整包里最容易被忽略、但工程价值最高的文件。它的结构就是{"图标名": "十六进制码点"},键名去掉前缀就是你写在 HTML 里的bi-后面的部分。做子集化、做自建图标选择器、做升级对比,全部依赖这张表。

2.2 字体图标、sprite、内联 SVG 的边界在哪

三种用法的底层原理完全不同,直接决定了你能不能用、好不好维护。

字体图标的原理是:把每个图标画在一个私用区码位上,CSS 用::before的content把码点塞进去,浏览器用字体渲染管线把它画出来。好处是着色直接跟color走、尺寸跟font-size走、写起来就是一个<i>标签,模板里改起来最省事;代价是它本质是文字,遇到字体加载失败就是一片方块,抗锯齿和基线对齐在部分系统上不一致,而且每个图标都要一个伪元素,content的值没法被 tree-shaking 分析。

SVG sprite 的原理是:把所有<symbol>塞进一个文件,页面里用<use href="sprite.svg#图标名">引用。好处是同一份图形可以被浏览器缓存、多次复用、按需引入某一个<symbol>,着色靠fill="currentColor"继承;代价是跨域和file://协议下外部引用会失效,必须走 HTTP,Safari 对跨域 sprite 的支持也一直比较保守。

内联 SVG 就是把<path>直接写进组件。好处是零网络请求、可以部分着色(双色图标)、可以加动效、无障碍可控;代价是模板体积大、重复图标会重复输出、改图标要改代码。

我的判断标准很朴素:后台管理类项目、图标用量大且样式统一,走字体图标或 sprite;组件库、需要多色或动效,走内联组件;已经上了打包器且能控制资源输出路径的,sprite 通常比字体更稳。下面按这三条路线分别落地。

3. 三条接入路线:从一行 link 到组件封装

这一章是能直接抄的部分。三条路线不是互斥的,同一个项目里混用很正常:管理后台主体用字体图标,登录页那个需要两种颜色的 Logo 用内联 SVG。关键是每条路线都要知道「哪几个文件必须一起搬」。

3.1 字体图标:把基础规则和码点规则分开看

最省事的做法是把font/整个目录拷到项目的静态资源目录,然后引入 CSS:

<!-- 假设你把 font 目录整个拷到了 public/vendor/icons/ 下 --> <link rel="stylesheet" href="/vendor/icons/bootstrap-icons.css"> <!-- 用的时候:基础类 bi 必须写,后面跟具体图标名 --> <i class="bi bi-gear"></i> <button class="btn"><i class="bi bi-trash"></i> 删除</button>

但依赖官方的 CSS 文件有个问题:它的@font-face里字体路径是相对于 CSS 文件位置的./fonts/,一旦你的构建工具把 CSS 抽离到别的目录、或者你把字体单独放 CDN,路径就对不上了。我一般会把这部分规则摘出来自己重写一遍,只保留真正起作用的那几条:

/* 自建 icons.css:路径、字体族名、缓存策略全部自己控制 */ @font-face { font-family: "bootstrap-icons"; /* 加 hash 查询串,避免换版本后浏览器继续用旧字体 */ src: url("./fonts/bootstrap-icons.woff2?v=1.11.3") format("woff2"), url("./fonts/bootstrap-icons.woff?v=1.11.3") format("woff"); font-display: block; /* 图标字体不要用 swap,闪一下方块比闪一下错位好看 */ font-weight: normal; font-style: normal; } /* 基础类:承担字体族、斜体归正、基线微调三件事 */ .bi::before, [class^="bi-"]::before, [class*=" bi-"]::before { display: inline-block; font-family: "bootstrap-icons" !important; font-style: normal; font-weight: normal !important; font-variant: normal; line-height: 1; vertical-align: -0.125em; text-transform: none; -webkit-font-smoothing: antialiased; } /* 码点规则:从 bootstrap-icons.json 里查出来抄进来,或者直接引官方 css 的这段 */ .bi-gear::before { content: "\f3e5"; } .bi-trash::before { content: "\f5de"; }

逻辑说明:三条选择器覆盖了三种写法——单独写bi的、bi-xxx开头的、以及class="btn bi-x"这种中间带空格的。font-style: normal是为了对抗部分 CSS reset 里把i标签设成斜体的规则;vertical-align: -0.125em是官方调出来的基线补偿值,删掉之后图标会比文字高半格;font-display: block是血泪经验,用swap的话首屏会先渲染出码点对应的乱码字符,肉眼可见地跳一下。

参数说明:content值不要自己猜,一定要去bootstrap-icons.json里查。写错一个十六进制位,页面上就是一个「豆腐块」或者别的图标,而且控制台一声不吭。另外注意[class^="bi-"]这条选择器对类名顺序敏感,class="bi-gear"能命中,class="text-lg bi-gear"就只能靠第三条带空格的规则命中,两条都保留才安全。

3.2 SVG sprite:一次引入,按 id 取用

sprite 方案的关键是那个根目录的bootstrap-icons.svg。把它丢到public/下,用<use>引用:

<!-- 图标尺寸靠 svg 的 width/height 控制,颜色靠 currentColor 继承父级 color --> <svg class="icon" width="16" height="16" fill="currentColor" aria-hidden="true"> <use href="/icons/bootstrap-icons.svg#gear"></use> </svg> <!-- 放在一个红色按钮里,图标自动变红,不需要额外写 CSS --> <button class="btn btn-danger"> <svg width="16" height="16" fill="currentColor" aria-hidden="true"> <use href="/icons/bootstrap-icons.svg#trash"></use> </svg> 删除 </button>

逻辑说明:<use>的href里#后面跟的就是 sprite 文件里<symbol>的 id,和icons/目录下的文件名严格一致。fill="currentColor"是让图形继承 CSS 的color属性,这也是为什么第二个例子里图标会跟着按钮变红——fill不写死颜色,只声明「跟随当前文字色」。

参数说明:这里有两个边界必须提前知道。第一,外部 sprite 在file://协议下不生效,双击 HTML 打开会看到空白,必须起一个本地 HTTP 服务;第二,跨域引用 sprite 在 Safari 上会被拦,如果 sprite 放在 CDN,要么把它改到同源,要么退回到字体方案。另外width/height建议写死成 16 或 16 的整数倍,这套图标的设计网格是 16,非整数倍缩放会糊。

3.3 内联与组件化:把图标变成一个带 props 的组件

需要双色、需要 hover 时局部变色、或者你在写一个对外发布的组件库,前两种方案就不够用了。这时把 sprite 当数据源,封一个薄组件:

// Icon.jsx —— React 版本,核心思路在 Vue/Svelte 里一样 export function Icon({ name, size = 16, title, className = "", ...rest }) { // 有 title 时是语义图标,要暴露给读屏;没有时纯装饰,直接隐藏 const a11y = title ? { role: "img", "aria-label": title } : { "aria-hidden": "true" }; return ( <svg className={`icon ${className}`} width={size} height={size} fill="currentColor" {...a11y} {...rest} > {title && <title>{title}</title>} <use href={`/icons/bootstrap-icons.svg#${name}`} /> </svg> ); } // 用法:<Icon name="gear" /> / <Icon name="exclamation-triangle" title="警告" />

逻辑说明:name直接映射 sprite 的 id,不做任何白名单校验的话,拼错就是静默空白,所以实际项目里我会在开发环境加一句if (import.meta.env.DEV && !iconNames.has(name)) console.warn(...),iconNames从bootstrap-icons.json的键生成,构建时注入。这样拼错图标名在开发阶段就会在控制台炸出来,而不是等测试提 bug。

参数说明:size默认 16,和设计网格对齐;title有值时才渲染<title>并设置role="img",否则整个 svg 标aria-hidden。这个区分很重要——装饰性图标如果被读屏念出来,一个「删除」按钮会被读成「垃圾桶 删除」,体验很差;反之纯图标按钮如果不加aria-label,读屏用户根本不知道这按钮是干嘛的。这块在第 6 章还会展开。

4. 裁剪与定制:把两千多个图标塞进构建链

直接用官方 CSS 的代价是:两千多条::before规则全部进包,虽然 gzip 之后不算大,但你项目里实际用到的可能就三四十个。更麻烦的是字体文件本身——woff2 全量大概一百多 KB,对首屏有要求的产品来说这笔钱花得冤枉。这一章讲怎么按需裁剪、怎么避免类名污染、以及打包器里字体资源怎么配。

4.1 子集化:用 codepoint 表把字形砍到只剩用到的

思路很直接:扫一遍源码里出现的bi-xxx类名,去bootstrap-icons.json里查出码点,然后用 fontTools 只保留这些码点重新生成字体。先扫:

# scan_icons.py —— 扫描源码里用到哪些图标,输出码点列表 import json, re, pathlib ROOT = pathlib.Path("bootstrap-icons") # 解压后的包目录 SRC = pathlib.Path("src") # 你的源码目录 EXT = {".html", ".vue", ".jsx", ".tsx", ".js", ".ts"} codes = json.loads((ROOT / "font/bootstrap-icons.json").read_text(encoding="utf-8")) used, missing = set(), set() for f in SRC.rglob("*"): if f.suffix not in EXT: continue text = f.read_text(encoding="utf-8", errors="ignore") # 匹配 bi-xxx 形式的类名,xxx 允许小写字母、数字、连字符 for name in re.findall(r"\bbi-([a-z0-9-]+)\b", text): if name in codes: used.add(name) else: missing.add(name) # 拼错,或者动态拼接出来的名字 print(f"命中 {len(used)} 个图标") if missing: print("以下名字在 1.11.3 里不存在,检查拼写或版本:", sorted(missing)) # 输出给 fontTools.subset 用的参数,形如 U+F3E5,U+F5DE print("--unicodes=" + ",".join(f"U+{codes[n].upper()}" for n in sorted(used)))

逻辑说明:先扫源码再查表,而不是反着来,是因为只有源码里出现的才是真需求。missing这个集合要单独打出来——动态拼接的类名(比如bi-${type})会被扫成type这种不存在的键,这类情况不能靠扫描解决,得靠人工确认后写进一个whitelist.txt一起合并。

参数说明:正则里\b边界不能省,否则bi-arrow-left会被bi-arrow先匹配掉。文件类型按你项目的实际情况加减,SCSS 文件里也可能出现@extend .bi-gear。

拿到码点列表之后跑子集化:

# woff2 输出依赖 brotli,先装好 pip install "fonttools[woff]" brotli python -m fontTools.subset \ bootstrap-icons/font/fonts/bootstrap-icons.woff2 \ --unicodes=U+F3E5,U+F5DE,U+F2B2 \ --flavor=woff2 \ --layout-features='' \ --no-hinting \ --desubroutinize \ --output-file=public/fonts/bi-subset.woff2

参数说明:--unicodes就是上一步的输出,直接粘进去;--flavor=woff2保证输出格式不变,删掉这个参数会输出 ttf;--layout-features=''去掉 OpenType 特性表,图标字体不需要 ligature 和 kerning;--no-hinting省掉字形微调数据,图标都是几何图形,用不上。跑完之后用ls -lh对比一下体积,通常能从一百多 KB 掉到几 KB。CSS 那边只保留用到的那些::before规则,可以让脚本一并生成,也可以手工留三十来行。

提示:子集化之后一定要清缓存重新验证一遍。字体文件名变了但 CSS 里 url 没加版本号,用户浏览器可能还在用旧的全量字体,表现为「新加的图标显示成方块」。我习惯把子集化的输出文件名带上内容 hash。

4.2 用 PostCSS 给图标类名加作用域

.bi这个前缀短得有点危险。如果你的页面里同时引入了另一个第三方图标库、或者某个老模块里自己定义了.bi类(真的见过,是「border-inner」的缩写),样式就会互相污染。官方 SCSS 里字体族名和字体目录是变量,但选择器前缀是写死的,改不了。

这时别去手改官方文件(升级时会全部覆盖掉),用 PostCSS 在选择器层面加作用域更省心:

// postcss-icon-scope.js —— 只给图标相关选择器加作用域前缀 module.exports = (opts = {}) => { const scope = opts.scope || ".app"; return { postcssPlugin: "postcss-icon-scope", Rule(rule) { // 跳过 @font-face 内部(那是 AtRule,本来就不会进这里),只处理类选择器 if (!rule.selector.includes(".bi")) return; rule.selector = rule.selector.replace( /(^|,\s*)(\.bi(?:-[\w-]+)?)/g, `$1${scope} $2` ); }, }; }; module.exports.postcss = true;

逻辑说明:PostCSS 8 的 visitor 写法里,Rule钩子在每条规则解析后被调用,直接改rule.selector就会写回产物。正则里(^|,\s*)处理选择器列表(a, .bi-x这种),保证每个逗号分段都能被加上前缀。

参数说明:scope传你的根容器类名,比如.app、#root。注意加作用域之后,图标类必须写在容器内部才生效,如果你的图标被挂到document.body下的全局弹层里(很多 UI 库的 Modal 会这么干),就会失效——这类场景要么给弹层也加上同样的容器类,要么把 scope 放宽成body。

再配合 SCSS 变量改字体族名,避免和内部另一套图标字体撞名:

// 覆盖默认变量后再引入,变量是 !default 的,先赋值生效 $bootstrap-icons-font: "acme-icons"; // 字体族名改成自己的 $bootstrap-icons-font-dir: "/fonts"; // 字体目录换成绝对路径,绕开相对路径问题 @import "bootstrap-icons/font/bootstrap-icons";

这三行要在@import之前写,且变量声明顺序很关键——SCSS 的!default是「未被赋值时才赋值」,反了就白改。改完之后记得同步@font-face里的font-family,两边必须一致,否则浏览器找不到字体会静默回退到系统字体,图标变成乱码字符。

4.3 构建链路里的字体资源

字体文件在打包器里是「资源」不是「代码」,输出路径和 hash 策略要单独配,否则升级一次版本,用户缓存里全是旧字体。

Vite 下我一般这么写:

// vite.config.js export default { build: { // 字体不要内联成 base64,单独出文件才能被 CDN 长缓存 assetsInlineLimit: 0, rollupOptions: { output: { assetFileNames: (info) => { const name = info.name || ""; // 字体统一放 fonts/ 下,其余静态资源走默认的 assets/ if (/\.(woff2?|ttf|eot)$/.test(name)) { return "fonts/[name]-[hash][extname]"; } return "assets/[name]-[hash][extname]"; }, }, }, }, };

Webpack 5 的写法对应是asset/resource加generator.filename:

// webpack.config.js 的 module.rules 里加一条 { test: /\.(woff2?|ttf|eot)$/, type: "asset/resource", generator: { filename: "fonts/[name].[contenthash:8][ext]", }, }

逻辑说明:两条配置干的是同一件事——把字体从 JS/CSS 的依赖图里拆出来,落到固定的fonts/目录,文件名带内容 hash。带 hash 的意义不只是缓存,更是能在升级时通过文件名一眼看出字体到底有没有被替换掉。

参数说明:Vite 那边把assetsInlineLimit设成 0 是刻意的,默认小于 4KB 的资源会被内联成 base64,而子集化之后的图标字体很可能就几 KB,一旦被内联,CSS 里就再也看不到字体 URL,排查 404 时你会抓瞎。Webpack 的contenthash:8取 8 位足够,太长会让 HTML 里的 CSS 引用变丑。

5. 排查清单:图标不显示的五个现场

这一章全部来自实际踩过的坑,按「现象 → 原因 → 解决」写。遇到图标问题时建议按这个顺序过一遍,比漫无目的地翻 DevTools 快得多。

5.1 现象:页面上一片空白或方块,Network 里字体请求 404

原因几乎总是路径。bootstrap-icons.css里的url("./fonts/xxx.woff2")是相对 CSS 文件自身位置的,你的构建工具把 CSS 抽到assets/下、字体丢到fonts/下,相对关系就断了。另一种情况是把字体放到了 public 目录但 CSS 里写的还是相对路径,开发环境能跑,因为 dev server 会做路径兜底,生产环境就 404。

解决:打开 Network 面板筛font,看请求的实际 URL 和你期望的差几层目录。我一般直接改成绝对路径/fonts/bootstrap-icons.woff2,并且在 CSS 里保留 hash 查询串。如果是 CDN 场景,记得配Access-Control-Allow-Origin,字体是跨域资源,缺这个头浏览器会拒绝使用。

5.2 现象:图标显示出来了,但是斜的、或者和文字不在一条基线上

原因是只引了码点规则(.bi-gear::before),没有引基础规则(.bi::before那几条)。有些同学为了瘦身,把 CSS 里带font-family的那段当冗余删了,结果字体回退到系统字体,content的私用区码点渲染成别的字形或者干脆是原始字符,同时因为i标签默认斜体,看起来就是歪的。

解决:基础规则一条都不能少,尤其font-style: normal、line-height: 1、vertical-align: -0.125em这三条。基线偏移量如果和你的字号不匹配,可以按字号微调,12px 字号用-0.15em通常更贴合。

5.3 现象:类名写对了,控制台也没报错,就是不出图标

原因是这个图标在 1.11.3 里根本不存在。图标库每个版本都在增删改名,你在某篇博客里抄到的bi-xxx可能是 1.12 才加的,或者已经被合并改名了。字体方案下这种情况完全静默——CSS 里没有对应规则,::before不生成内容,页面上就是什么都没有。

解决:去bootstrap-icons.json里搜这个名字的键。搜不到就是版本问题,要么换图标,要么升级包版本。这也是为什么第 3 章的组件里要在开发环境加白名单校验,把静默失败提前成显式警告。

5.4 现象:改color没反应,图标永远是黑的

用 sprite 或内联 SVG 时,如果<path>或<symbol>上写了fill="#000"这种具体颜色,父级的color就传不进去。字体图标这边则是另一种情况:某些全局样式里有.bi { color: #333 !important }之类的规则,权重压过了你的类。

解决:SVG 场景统一用fill="currentColor",不要在任何一层写具体颜色值;字体场景用 DevTools 的 Computed 面板看 color 的最终来源,找到那条带!important的规则,要么提高自己的选择器权重,要么把那条规则干掉。顺带一提,currentColor连opacity都能继承良好,做禁用态很方便。

5.5 现象:本地开发一切正常,构建产物里图标全丢

原因是打包器把字体当静态资源处理后,CSS 里的引用被改写成了一个带 hash 的路径,但那次改写发生在 CSS 抽离插件之前,最终产物里 CSS 引用的文件名和实际输出的文件名对不上。另一种常见变体是字体被打进了 JS chunk,CSS 里引用的是data:内联,看起来能用,但因为assetsInlineLimit的关系,字体被内联之后体积反而变大,且无法长缓存。

解决:构建完之后grep一下产物 CSS 里的@font-face,看 url 指向哪里,再去dist目录确认那个文件真实存在。这一步花三十秒,能省掉一次线上回归。我在 CI 里加了一条检查:产物 CSS 里所有url(...)引用的文件必须存在,不存在就 fail。

6. 把图标当 API 管:版本 diff 与无障碍标注

图标库升级是最容易被低估的风险点。1.11.3 升到下一个版本,包体积、图标名、码点都可能变,而你项目里几百处bi-xxx是散落在模板里的,没人知道哪些会被影响。我的做法是在升级前跑一次 diff,把两版的icons/目录和 json 拉出来对比:

# 假设老版本解压在 bootstrap-icons-old/,新版本在 bootstrap-icons-new/ python - <<'PY' import json, pathlib def load(p): d = pathlib.Path(p) / "font/bootstrap-icons.json" return json.loads(d.read_text(encoding="utf-8")) old, new = load("bootstrap-icons-old"), load("bootstrap-icons-new") added = sorted(new.keys() - old.keys()) removed = sorted(old.keys() - new.keys()) # 名字还在但码点变了,字体方案下必须同步更新 CSS moved = sorted(k for k in old.keys() & new.keys() if old[k] != new[k]) print(f"新增 {len(added)},删除 {len(removed)},码点变动 {len(moved)}") print("删除(重点排查):", removed[:20]) print("码点变动(字体方案必须同步):", moved[:20]) PY

这段脚本的价值在最后一行。新增和删除都好办——删了顶多换个图标,加了不用也没事;真正会让线上翻车的是码点变动:你复用了旧的bootstrap-icons.css,字体却更新了,那么content: "\f3e5"指向的可能是完全另一个图形,页面上没有任何报错,只是图标含义悄悄变了。所以字体方案升级时,CSS 和字体文件必须成对替换,我在脚本最后加一句断言,moved非空且我没更新 CSS 就直接退出码 1。

无障碍这块,判断标准就一条:这个图标是不是独立承载了信息。纯装饰(按钮旁边配的文字已经说明了功能)一律aria-hidden="true",让读屏跳过;独立按钮(只有图标没有文字的关闭、删除、展开)必须给可访问名称,组件里用title属性我会渲染成<title>加role="img",也可以直接给按钮加aria-label="关闭",两种都行,但一个项目里要统一。字体图标有个额外注意点:<i class="bi bi-x"></i>在没有aria-hidden的情况下,某些读屏会把私用区码点念成一串奇怪的数字,所以字体方案下我习惯给所有图标默认加aria-hidden="true",需要语义的地方单独处理。

从那以后,我每次升级图标库都强制走一遍「跑 diff 脚本 → 看 removed 和 moved → 同步 CSS 和字体 → 本地起服务肉眼扫一遍关键页面」这四步,宁可多花十分钟,也不想再经历一次凌晨被叫起来看「删除按钮变成星星」的现场。希望帮到你。

本文还有配套的精品资源,点击获取