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.cssfont-face定义 每个图标的::before码点规则直接link引入或用打包器 importfont/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 hrefsprite.svg#图标名引用。好处是同一份图形可以被浏览器缓存、多次复用、按需引入某一个symbol着色靠fillcurrentColor继承代价是跨域和file://协议下外部引用会失效必须走 HTTPSafari 对跨域 sprite 的支持也一直比较保守。内联 SVG 就是把path直接写进组件。好处是零网络请求、可以部分着色双色图标、可以加动效、无障碍可控代价是模板体积大、重复图标会重复输出、改图标要改代码。我的判断标准很朴素后台管理类项目、图标用量大且样式统一走字体图标或 sprite组件库、需要多色或动效走内联组件已经上了打包器且能控制资源输出路径的sprite 通常比字体更稳。下面按这三条路线分别落地。3. 三条接入路线从一行 link 到组件封装这一章是能直接抄的部分。三条路线不是互斥的同一个项目里混用很正常管理后台主体用字体图标登录页那个需要两种颜色的 Logo 用内联 SVG。关键是每条路线都要知道「哪几个文件必须一起搬」。3.1 字体图标把基础规则和码点规则分开看最省事的做法是把font/整个目录拷到项目的静态资源目录然后引入 CSS!-- 假设你把 font 目录整个拷到了 public/vendor/icons/ 下 -- link relstylesheet href/vendor/icons/bootstrap-icons.css !-- 用的时候基础类 bi 必须写后面跟具体图标名 -- i classbi bi-gear/i button classbtni classbi 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?v1.11.3) format(woff2), url(./fonts/bootstrap-icons.woff?v1.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开头的、以及classbtn bi-x这种中间带空格的。font-style: normal是为了对抗部分 CSS reset 里把i标签设成斜体的规则vertical-align: -0.125em是官方调出来的基线补偿值删掉之后图标会比文字高半格font-display: block是血泪经验用swap的话首屏会先渲染出码点对应的乱码字符肉眼可见地跳一下。参数说明content值不要自己猜一定要去bootstrap-icons.json里查。写错一个十六进制位页面上就是一个「豆腐块」或者别的图标而且控制台一声不吭。另外注意[class^bi-]这条选择器对类名顺序敏感classbi-gear能命中classtext-lg bi-gear就只能靠第三条带空格的规则命中两条都保留才安全。3.2 SVG sprite一次引入按 id 取用sprite 方案的关键是那个根目录的bootstrap-icons.svg。把它丢到public/下用use引用!-- 图标尺寸靠 svg 的 width/height 控制颜色靠 currentColor 继承父级 color -- svg classicon width16 height16 fillcurrentColor aria-hiddentrue use href/icons/bootstrap-icons.svg#gear/use /svg !-- 放在一个红色按钮里图标自动变红不需要额外写 CSS -- button classbtn btn-danger svg width16 height16 fillcurrentColor aria-hiddentrue use href/icons/bootstrap-icons.svg#trash/use /svg 删除 /button逻辑说明use的href里#后面跟的就是 sprite 文件里symbol的 id和icons/目录下的文件名严格一致。fillcurrentColor是让图形继承 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} fillcurrentColor {...a11y} {...rest} {title title{title}/title} use href{/icons/bootstrap-icons.svg#${name}} / /svg ); } // 用法Icon namegear / / Icon nameexclamation-triangle title警告 /逻辑说明name直接映射 sprite 的 id不做任何白名单校验的话拼错就是静默空白所以实际项目里我会在开发环境加一句if (import.meta.env.DEV !iconNames.has(name)) console.warn(...)iconNames从bootstrap-icons.json的键生成构建时注入。这样拼错图标名在开发阶段就会在控制台炸出来而不是等测试提 bug。参数说明size默认 16和设计网格对齐title有值时才渲染title并设置roleimg否则整个 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(encodingutf-8)) used, missing set(), set() for f in SRC.rglob(*): if f.suffix not in EXT: continue text f.read_text(encodingutf-8, errorsignore) # 匹配 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 用的参数形如 UF3E5,UF5DE print(--unicodes ,.join(fU{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 \ --unicodesUF3E5,UF5DE,UF2B2 \ --flavorwoff2 \ --layout-features \ --no-hinting \ --desubroutinize \ --output-filepublic/fonts/bi-subset.woff2参数说明--unicodes就是上一步的输出直接粘进去--flavorwoff2保证输出格式不变删掉这个参数会输出 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 场景统一用fillcurrentColor不要在任何一层写具体颜色值字体场景用 DevTools 的 Computed 面板看 color 的最终来源找到那条带!important的规则要么提高自己的选择器权重要么把那条规则干掉。顺带一提currentColor连opacity都能继承良好做禁用态很方便。5.5 现象本地开发一切正常构建产物里图标全丢原因是打包器把字体当静态资源处理后CSS 里的引用被改写成了一个带 hash 的路径但那次改写发生在 CSS 抽离插件之前最终产物里 CSS 引用的文件名和实际输出的文件名对不上。另一种常见变体是字体被打进了 JS chunkCSS 里引用的是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(encodingutf-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-hiddentrue让读屏跳过独立按钮只有图标没有文字的关闭、删除、展开必须给可访问名称组件里用title属性我会渲染成title加roleimg也可以直接给按钮加aria-label关闭两种都行但一个项目里要统一。字体图标有个额外注意点i classbi bi-x/i在没有aria-hidden的情况下某些读屏会把私用区码点念成一串奇怪的数字所以字体方案下我习惯给所有图标默认加aria-hiddentrue需要语义的地方单独处理。从那以后我每次升级图标库都强制走一遍「跑 diff 脚本 → 看 removed 和 moved → 同步 CSS 和字体 → 本地起服务肉眼扫一遍关键页面」这四步宁可多花十分钟也不想再经历一次凌晨被叫起来看「删除按钮变成星星」的现场。希望帮到你。本文还有配套的精品资源点击获取