做前端时间长了,你迟早会碰上这么一类需求:设计稿上一个小小的箭头,往右指、往下折、带描边或者实心的,看着毫不起眼,真动手写的时候却要在html小箭头图标和html箭头相关符号这两条路之间反复横跳。字符实体快是快,但一换设备就可能变成彩色表情或者方块;CSS 手绘够轻量,可尺寸算错就歪得离谱;SVG 最稳,又怕引入一堆冗余代码。这篇东西就是把这几种路子摊开来讲清楚:每种方案适合什么场景、参数怎么算、坑在哪、怎么排查,不管你是刚写完第一个静态页面的新手,还是天天维护组件库的老手,都能从里面挑到能直接抄的写法。
1. 先给箭头分个类,选型才不会乱
1.1 四种实现路线的本质差别
网页上的箭头,从实现机制上看只有四类,搞清楚它们的物理本质,后面所有选择都是水到渠成的事。第一类是字符型,也就是把箭头当成一个"字"打出来,浏览器用你指定的字体去渲染这个字形;第二类是绘制型,用 CSS 的border、transform、clip-path拼出来,本质是盒子模型和几何变换;第三类是矢量图形型,包括 inline SVG、<img>引用的 svg 文件、以及转成 data URI 的 SVG 背景图;第四类是字体图标型,把一堆图标打包成自定义字体,靠私有码位调用,这类方案前几年很流行,现在新项目里用得越来越少。
这四类的差别不是"高级程度",而是控制粒度和运行时成本。字符型几乎没有成本,一个字符占几个字节,但它受字体、系统、编码三重影响,你控制不了它的笔画粗细、端点圆角、精确对齐;CSS 绘制型完全可控,代价是代码量上来之后不好维护,而且只能画出简单几何形状;SVG 是形状描述,可以精确到小数点,能描边、能渐变、能动画,代价是需要额外标签或者文件;字体图标本质上和字符型同源,只是把"字"换成了自己设计的图形,它继承了字符型所有的对齐毛病,还额外背上了一个几百 KB 的字体文件。
我个人的判断标准很简单:**如果这个箭头是装饰性的、只出现一两次、尺寸跟随文字,字符实体完全够用;如果它出现在组件里、要跟随状态变化(比如展开收起旋转)、要做交互动效,直接上 SVG 或者 CSS 绘制,别犹豫。**中间那种"用字符实体但想要可控尺寸"的纠结状态,是最容易返工的。
| 路线 | 典型写法 | 可控性 | 体积 | 适合场景 | 主要风险 |
|---|---|---|---|---|---|
| 字符实体 | →/→ | 低 | 极小 | 正文里的方向说明、简单分隔符 | 字体回退、emoji 化、对齐漂移 |
| CSS 绘制 | border 三角形、旋转 V 形 | 中高 | 小 | 折叠面板、下拉箭头、tooltip 小尖角 | 尺寸要算、描边难做圆角 |
| SVG | inline<svg>/ sprite | 高 | 中 | 组件图标、需要动效和精确对齐 | 需要统一管理 symbol |
| 字体图标 | font-family: iconfont | 中 | 大 | 老项目、图标数量极多的后台系统 | 加载阻塞、子集化麻烦、无障碍差 |
1.2 按场景反推方案的决策表
光讲原理不够,实际开发中更有效的方式是拿场景去反查方案。我整理了一张经常被同事问到的清单,基本覆盖了日常工作里 90% 的箭头需求。注意这里的"推荐"不是唯一解,而是综合考虑了可维护性和踩坑概率之后的最优解;如果你手头的项目已经有成熟的图标体系,优先服从现有体系,别为了一个箭头引入第二套方案,那才是真正的技术债。
有一个经常被忽略的维度是这个箭头会不会被用户读屏软件念出来。装饰性箭头(比如面包屑之间的分隔)必须加aria-hidden="true",否则读屏软件可能念出"向右箭头",打断语义;而表达动作的箭头(比如"下一页"按钮上的箭头)反倒需要保证按钮本身有可读文本,箭头只是视觉补充。字符型箭头在这件事上特别讨厌,因为它天然是文本节点,会被朗读,而你很难给它单独加属性,只能靠外面包一层<span aria-hidden="true">。
| 场景 | 推荐方案 | 关键理由 |
|---|---|---|
| 面包屑分隔符 | 字符实体或 CSS 伪元素 | 出现频次高、纯装饰、尺寸小 |
| 折叠面板展开指示 | SVG 或 CSS chevron | 需要 90 度旋转过渡 |
| 下拉选择框箭头 | CSS 绘制或背景 SVG | 需要覆盖原生箭头,且要跟随主题色 |
| 表格列排序指示 | 两个三角形字符或 SVG | 需要上下双向、状态切换 |
| 按钮内的"前往"箭头 | SVG 或字符实体 | 常配套位移动效 |
| 正文里的方向描述 | 字符实体 | 语义化文本,跟随字号行高 |
| 回到顶部悬浮按钮 | SVG | 尺寸大、需要描边和圆形底 |
2. 字符实体这条线:最省事也最容易翻车
2.1 命名实体和码位的完整对照
HTML 里真正被规范收录的箭头命名实体其实没几个,很多同学以为→后面还能接着写↝、⇥,实际上大部分花式箭头根本没有命名实体,只能走数字引用或者直接粘贴 Unicode 字符。这一点必须先说清楚,不然你在编辑器里敲半天敲不出提示,还以为是编辑器坏了。
规范里和箭头直接相关的命名实体大致是这些:←(左)、↑(上)、→(右)、↓(下)、↔(左右双向),以及它们的大写双线版本⇐、⇑、⇒、⇓、⇔。另外还有一个比较冷门的↵,是那个带折角的回车箭头。除了这几个,剩下的箭头一律只能用&#xxxx;的十进制或十六进制写法。
| 符号 | 命名实体 | 十进制 | 十六进制 | Unicode 名称 |
|---|---|---|---|---|
| ← | ← | ← | ← | LEFTWARDS ARROW |
| ↑ | ↑ | ↑ | ↑ | UPWARDS ARROW |
| → | → | → | → | RIGHTWARDS ARROW |
| ↓ | ↓ | ↓ | ↓ | DOWNWARDS ARROW |
| ↔ | ↔ | ↔ | ↔ | LEFT RIGHT ARROW |
| ⇐ | ⇐ | ⇐ | ⇐ | LEFTWARDS DOUBLE ARROW |
| ⇒ | ⇒ | ⇒ | ⇒ | RIGHTWARDS DOUBLE ARROW |
| ⇔ | ⇔ | ⇔ | ⇔ | LEFT RIGHT DOUBLE ARROW |
| ↵ | ↵ | ↵ | ↵ | DOWNWARDS ARROW WITH CORNER LEFTWARDS |
| ➜ | 无 | ➜ | ➜ | HEAVY ROUND-TIPPED RIGHTWARDS ARROW |
| ➤ | 无 | ➤ | ➤ | BLACK RIGHTWARDS ARROWHEAD |
| ▶ | 无 | ▶ | ▶ | BLACK RIGHT-POINTING TRIANGLE |
| ▸ | 无 | ▸ | ▸ | BLACK RIGHT-POINTING SMALL TRIANGLE |
| ▼ | 无 | ▼ | ▼ | BLACK DOWN-POINTING TRIANGLE |
| ⟶ | 无 | ⟶ | ⟶ | LONG RIGHTWARDS ARROW |
这张表我建议直接存进笔记,尤其是十进制那一列。很多老代码库里写的都是十进制形式,你如果只会认命名实体,改代码时就得一个个去搜。
2.2 直接粘贴还是写实体:编码层面的取舍
很多人的习惯是直接从别的地方把→复制粘贴进 HTML 文件,这样源码看起来最直观。这么干本身没问题,前提是文件真的以 UTF-8 保存并且声明了<meta charset="utf-8">。我见过不止一次这样的情况:页面顶部忘了写 meta,本地打开一切正常,一部署到服务器就变成了乱码方块。原因是浏览器在没拿到编码声明时,会回退到本地默认编码去猜,而那个默认值在不同环境下并不一致。
写实体→或者→的好处是源码本身是纯 ASCII,无论文件被谁用什么编码打开,都不会变形,浏览器解析阶段会统一转成对应字符。这在多人协作、老系统、需要经过构建工具二次处理的项目里,稳定性明显更好。代价是可读性差一点,你得记住这些符号对应的实体名。
我的实际做法是:**结构简单、只出现一次的装饰箭头用实体;成批复用的图标走 SVG;正文里描述性的箭头直接粘贴字符。**另外提醒一句,某些编辑器会在保存时把连续的箭头字符识别成"特殊格式"并做自动替换,如果发现代码被改了,检查一下编辑器里的智能标点或者自动替换相关设置。
注意:如果你在 CSS 的
content属性里写箭头,必须用转义序列而不是实体。content: "→"是错的,浏览器会原样输出这串字符;正确写法是content: "\2192",而且十六进制转义后面最好跟一个空格,比如content: "\2192 ",否则转义会把它后面的字符一并吞掉当成十六进制位解析。
2.3 字体回退和 emoji 化,字符箭头最大的两个雷
字符箭头最让人头疼的问题是渲染不受控。同一个→,在 macOS 上可能是细笔画、在 Windows 上可能偏粗、在某些安卓机的默认中文字体里可能被渲染成一个宽度明显不同的字形,导致你精心调好的间距在别的设备上全乱。这不是浏览器的问题,是字体覆盖范围的问题——箭头属于"数学符号"区块,很多正文字体压根没收录,浏览器只能去字体栈里往下找,找到谁算谁。
解决思路是主动指定一个包含符号区块的字体栈,而不是依赖默认值。比如:
.symbol-arrow { font-family: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", "DejaVu Sans", sans-serif; font-variant-emoji: text; line-height: 1; display: inline-block; transform: translateY(0.05em); }这里的font-variant-emoji: text是个相对新的属性,作用是告诉浏览器"即使这个码位有彩色 emoji 版本,也请用文本形态渲染"。兼容性目前还不算全,所以还需要配合变体选择符一起用。Unicode 里有两个特殊码位:U+FE0E(文本呈现)和U+FE0F(emoji 呈现)。对于↔、➡这类既可能是文本又可能是彩色表情的字符,在它后面显式追加\FE0E就能强制走文本形态。在 HTML 里写↔︎,在 CSS 里写content: "\2194\FE0E"。
还有一种更隐蔽的情况:字符在某些系统上直接不显示,变成一个空心方块(俗称豆腐块)。这说明整条字体栈里没有一个字体覆盖这个码位。我的处理习惯是给这类符号单独包一层类名,字体栈里显式带上"Noto Sans Symbols 2"作为兜底,同时在设计阶段就尽量少用那些生僻的装饰性箭头——➜、➤这类确实是好看,但它们在移动端的表现比→不确定得多。
3. 纯 CSS 手绘箭头:把尺寸算明白
3.1 border 三角形的宽度高度推导
用border画三角形是个老技巧,但真正能把尺寸算对的人不多,因为它的尺寸规律和普通盒子完全不同。原理是:当一个元素的宽高都为 0,四边各有一定宽度的 border 时,四个 border 的交界处会形成 45 度斜切,于是每一条 border 都被切成了一个等腰梯形;如果把三条边的颜色设为透明,剩下的那一条就显示成三角形。
关键在于三角形的底边长度等于左右两条相邻 border 宽度之和,高度等于自身那条 border 的宽度。假设我要一个指向下的小三角,视觉上宽 12px、高 7px,那么代码就是:
.arrow-down { width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 7px solid currentColor; }底边 = 6 + 6 = 12px,高 = 7px,和需求对上。如果你直接把border-top设成 12px 想"宽高一样",画出来会是一个底 12、高 12 的三角形,视觉上会明显偏高、偏瘦,因为等边三角形的实际高是底边乘以 0.866,而不是等于底边。想要视觉上像等边三角形,border-top应该设成相邻 border 宽度的 1.732 倍左右。
这个技巧现在用得越来越少了,因为border三角形没法做圆角,没法做描边,也没法做渐变,而且currentColor虽然能跟随文字颜色,但没法做 hover 变色之外的过渡动画(颜色可以过渡,形状不行)。它的真正用武之地是 tooltip 的小尖角、气泡框的尾巴这类只需要一个实心色块的地方。
3.2 旋转正方形拼出 V 形箭头
比三角形更常用的是那个像"V"或者">"的折线箭头,英文里叫 chevron,折叠面板、下拉菜单、分页按钮上到处都是它。实现思路是:做一个正方形,只保留相邻的两条边框,然后旋转 45 度,两条边框就变成了一个尖角。
.chevron { width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg); transition: transform 0.25s ease; } .chevron.is-open { transform: rotate(-135deg); }尺寸的计算方式是这样的:正方形边长决定了箭头的"臂长",两条 border 的宽度决定了笔画粗细,旋转 45 度之后,箭头的开口朝向右下到左下之间,视觉高度是边长乘以 1.414(也就是对角线长度)。所以如果你要一个视觉上大约 11px 高的箭头,边长设成 8px 就差不多。这个数字我调过很多次,8px 边长配 2px 笔画,在 14px 到 16px 的正文里看着最舒服,笔画太细在低分屏上会发虚,太粗又显得笨重。
旋转方向这里有个容易踩的坑:默认状态如果想让箭头朝下(表示可展开),通常写成rotate(45deg),因为这样两条边构成的尖角指向右下方,视觉上更像朝下的 V;展开后旋转到-135deg就变成了朝上。这中间转的是 180 度,如果你的过渡动画看起来"绕远路",八成是起始角度设反了,从 45 度转到 -135 度在浏览器里是顺时针 180 度,视觉上没问题;但如果从 45 度转到 315 度,就会绕一大圈。
3.3 描边箭头:一条线加一个折角
设计稿上那种细长的、带箭杆的箭头,用纯 CSS 也能做,思路是拆成两个部分:一个横向的线段,加一个旋转后的 V 形箭头头部。线段可以用一个 1px 或 2px 高的div,头部还是用 3.2 里那套正方形旋转的办法。
.arrow-line { position: relative; display: inline-block; width: 18px; height: 2px; background: currentColor; } .arrow-line::after { content: ""; position: absolute; right: 0; top: 50%; width: 7px; height: 7px; border-top: 2px solid currentColor; border-right: 2px solid currentColor; transform: translateY(-50%) rotate(45deg); transform-origin: center; }这里有个细节必须注意:translateY(-50%)和rotate(45deg)的顺序不能反。如果写成rotate(45deg) translateY(-50%),平移的方向会被旋转矩阵带着一起转,箭头就会歪到侧边去。这是 CSS transform 里最容易犯的错误之一,记住"先定位再旋转"这个顺序基本不会错。
另外描边箭头在视觉上有个光学补偿问题:箭杆的粗细和箭头头部的笔画粗细如果在数值上完全相等,看起来箭头会显得更"瘦",因为斜线在视觉上显得更细。我一般的做法是把头部的 border 宽度比箭杆大 0.5 到 1px,或者干脆把整个箭头改用 SVG 画,SVG 里的stroke是均匀的,配合stroke-linecap: round和stroke-linejoin: round能一次解决所有端点问题。
4. SVG 路线:一次投入,长期省心
4.1 inline SVG 的最小可用写法
到了需要精确控制的时候,SVG 就是答案。一个箭头图标的最小 SVG 长这样:
<svg class="icon-arrow" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg>几个参数值得单独说。viewBox="0 0 24 24"是图标界的通用网格,几乎所有开源图标集都遵循这个尺寸,用它能保证不同来源的图标视觉大小一致。width="1em" height="1em"让图标跟随父元素字号缩放,比写死像素灵活得多,改字号的时候图标自动跟着变。fill="none"配合stroke="currentColor"是线性图标的标准组合,currentColor让图标继承文字颜色,hover 时只要改父元素的color,图标就自动变色,不需要额外写图标样式。
stroke-width="2"在 24 网格下大致相当于视觉上的"中等粗细",比它细会显得轻飘,比它粗会糊成一团。还有一点,SVG 里的stroke-width是按 viewBox 坐标算的,不是按渲染后的像素,所以当图标被放大到 48px 显示时,笔画也会等比放大,粗度变成约 4px。如果你希望大尺寸下笔画保持纤细,就得给不同尺寸准备不同stroke-width的图标,或者用非等比缩放的技巧,但后者会让形状变形,一般不推荐。
4.2 用 symbol + use 做图标复用
页面里箭头一多,每次都贴一段 SVG 代码会让模板变得又长又乱。比较优雅的做法是集中定义一次,其他地方用<use>引用:
<svg width="0" height="0" style="position:absolute" aria-hidden="true"> <defs> <symbol id="i-arrow-right" viewBox="0 0 24 24"> <path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </symbol> </defs> </svg> <svg class="icon" width="1em" height="1em" aria-hidden="true"> <use href="#i-arrow-right"></use> </svg><use>的引用方式有个历史上遗留的坑:老版本的写法是xlink:href,需要额外声明命名空间,现代浏览器都支持href,但如果你要兼容一些老的框架或者需要支持 IE,那就两个都写上。另外,<use>引用的内容在样式继承上有微妙差别:currentColor会正常继承,但如果你在 symbol 内部写死了某个类名并想通过外部选择器改样式,跨 shadow boundary 是不会生效的。所以规范做法是:symbol 里只写结构,颜色和粗细全靠currentColor和stroke-width这类可继承属性传递。
4.3 背景图、data URI 与 mask-image 变色
有时候箭头不能写成标签,比如要给原生<select>换箭头、给background加装饰,这时候就得用背景图。SVG 可以直接内联成 data URI:
.select-custom { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px 16px; padding-right: 40px; }写 data URI 时两个地方最容易出错:一是#必须编码成%23,否则会被当成 URL 片段标识符,后面整段都被截断;二是引号的处理,外层如果用了双引号,内部就得用单引号,反过来的规则一样。颜色写死是一个限制,所以更灵活的做法是mask-image:
.icon-mask { width: 16px; height: 16px; background-color: currentColor; mask-image: url("data:image/svg+xml,..."); mask-repeat: no-repeat; mask-position: center; mask-size: contain; }mask只取形状不取颜色,图形本身的颜色会被background-color覆盖,于是currentColor又能生效了,hover 变色、暗色模式切换全都不用改代码。代价是兼容性和性能开销都比 inline SVG 略高一点点,用在数量少的场景完全没问题。
5. 落地场景:四个高频位置的具体写法
5.1 面包屑里的分隔符
面包屑的分隔箭头是典型的纯装饰元素,读写屏软件绝对不该读到它。最干净的写法是用伪元素,HTML 结构里一个箭头字符都不出现:
<nav aria-label="面包屑"> <ol class="breadcrumb"> <li><a href="/">首页</a></li> <li><a href="/list">列表</a></li> <li aria-current="page">详情</li> </ol> </nav>.breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .breadcrumb li + li::before { content: "\2192"; margin-right: 8px; color: #999; font-size: 0.9em; }用::before而不是::after,是为了保证分隔符天然只出现在"除第一项之外的项"前面,省掉了:last-child的排除逻辑。gap和margin-right同时用看起来有点重复,但gap控制的是li之间的距离,margin-right控制的是箭头和下一个文字之间的距离,两者作用对象不同,一起写反而更稳定,因为不同字体下箭头的左右边距并不对称。
如果你的设计稿要求分隔符是斜线或者 V 形,那就别用字符了,直接换成一个 5px 见方的旋转 div,或者干脆用border-right做一个 45 度斜线,尺寸更好控。
5.2 折叠面板的箭头旋转与状态同步
折叠面板的箭头要跟随展开状态转动 90 度,这里的核心不是箭头本身,而是状态从哪来。如果用<details>元素,可以靠[open]属性做纯 CSS 控制:
details > summary { list-style: none; cursor: pointer; } details > summary::-webkit-details-marker { display: none; } details > summary::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(-45deg); transition: transform 0.2s ease; } details[open] > summary::before { transform: rotate(45deg); }注意这里初始是-45deg(尖角朝右,表示"可展开"),展开后是45deg(尖角朝下,表示"内容已展开"),这是符合常见交互直觉的方向。如果你用的是自定义的button加 JS 切换类名,逻辑一样,只是选择器换成类名。我踩过的一个坑是:给伪元素加transition时只写了transform,但display: inline-block的元素在旋转时会有 0.5px 的亚像素抖动,加一句will-change: transform或者干脆给父元素加backface-visibility: hidden都能缓解。
5.3 表格排序和自定义下拉箭头
表格表头的排序指示通常需要上下两个小三角,用纯 CSS 三角形的组合最省事:
.sort-indicator { display: inline-flex; flex-direction: column; gap: 2px; vertical-align: middle; margin-left: 4px; } .sort-indicator i { width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; } .sort-indicator i:first-child { border-bottom: 5px solid #bbb; } .sort-indicator i:last-child { border-top: 5px solid #bbb; } .sort-asc i:first-child { border-bottom-color: #1a73e8; } .sort-desc i:last-child { border-top-color: #1a73e8; }用currentColor当然更省事,但表格排序经常需要对"未激活"和"已激活"区分颜色,用具体的颜色值配合状态类更直观。这里的gap: 2px不能省,两个三角贴在一起会变成一个菱形,视觉上完全变味。
下拉框的箭头则是另一套逻辑。原生<select>的箭头在跨浏览器时差异极大,macOS 上的样式和 Windows 上完全不同,想要统一就必须appearance: none把它干掉,然后用背景图或者伪元素重画。注意<select>元素本身不支持::after伪元素,所以只能走背景图那条路,这也是为什么前面讲 data URI 时说它最适合这个场景。
6. 排查实录:那些年箭头给我挖的坑
6.1 箭头不居中、粗细忽粗忽细
**症状一:字符箭头和旁边文字对不齐,总是偏上或偏下。**根本原因是箭头字符的基线位置和汉字不一样,汉字是方块字占满整个字身,箭头是一个位于中间偏上位置的符号。硬调vertical-align: middle往往只能改善一点。我一般用的办法是给箭头单独包一个span,设display: inline-block; transform: translateY(0.06em);,微调幅度在 0.05em 到 0.1em 之间,具体看字号。这个值不能写死成像素,因为字号一变比例就崩了。
**症状二:同一个箭头在不同字号下粗细不一致。**这是字体渲染的特性,不是 bug。中文字体在很小时会做 hinting 处理,笔画粗细会被量化到像素网格,所以 12px 和 13px 的箭头可能看起来粗细差很多。规避办法是把箭头图标和字号解耦,用 SVG 加固定stroke-width,让笔画按比例缩放,视觉一致性反而更好。
**症状三:CSS 三角形上下左右的尖角对不上。**大概率是四条 border 的宽度比例不对。指向下的三角只有三条边有色,如果左右透明边的宽度不等,三角就会偏。检查时可以在开发者工具里临时给透明边加上半透明颜色,一眼就能看出来。
6.2 显示成方块或者彩色表情
这两个问题在排查时经常被混为一谈,其实原因完全不同。
变成方块(豆腐块),意味着整条字体栈里没有任何字体覆盖这个码位。排查步骤是:先在开发者工具的 Computed 面板里看font-family实际用的是什么,然后在 Elements 里选中那个字符,看渲染宽度是不是异常。快速验证的办法是在控制台里跑一段 canvas 测量:
const c = document.createElement('canvas').getContext('2d'); c.font = '16px sans-serif'; console.log(c.measureText('\u2192').width); // 与预期的汉字宽度对比,明显偏小或为 0 说明字体没覆盖变成彩色表情,说明这个码位有 emoji 变体,且系统默认选了彩色呈现。→(U+2192)一般不会,但↔(U+2194)、⬅(U+2B05)、➡(U+27A1)都是重灾区。解决办法前面提过,在字符后追加\FE0E强制文本呈现,或者用font-variant-emoji: text。如果这两个都不生效,最稳妥的方案就是换成 SVG——毕竟你没法控制别人的系统。
6.3 高 DPI、打印、暗色模式下的表现
高 DPI 屏幕上的模糊,几乎只发生在位图方案上。如果你的箭头是用 PNG 图片做的背景,在 2 倍屏上会被拉糊,必须准备2x图或者干脆换成 SVG。SVG 是矢量描述,任何像素密度下都是锐利的,这是它在图标场景里最硬的优势。
打印时箭头消失,原因是浏览器默认不打印背景图和背景色。如果你的箭头是靠background-image画的,打印出来就是空白。解决方法有两种:一是关键图标改用 inline SVG(它会随 DOM 一起打印),二是给容器加print-color-adjust: exact,强制浏览器保真打印背景。但后者在部分浏览器里需要-webkit-前缀,且用户可以在打印设置里覆盖,不算绝对可靠。
暗色模式下的对比度,这个坑最隐蔽。用写死的十六进制颜色的箭头(比如stroke='%23666'),在深色背景上几乎看不见。正确做法是全链路用currentColor,让箭头的颜色始终等于文字颜色,文字颜色本身再由主题变量控制,这样主题一换,箭头自动跟着换,不需要维护第二套样式。
6.4 箭头问题速查表
| 现象 | 最可能的原因 | 快速验证 | 处理方式 |
|---|---|---|---|
| 显示成空心方块 | 字体未覆盖该码位 | canvas measureText 宽度异常 | 补全符号字体栈或改 SVG |
| 显示成彩色表情 | 系统选了 emoji 呈现 | 检查是否有 FE0F | 加 FE0E 或改 SVG |
| 尺寸不跟随字号 | 写死了 px 宽高 | 改父元素 font-size 观察 | 改用 em 或 1em 尺寸 |
| 上下对不齐 | 字符基线位置不同 | 和汉字并排截图对比 | inline-block 加 translateY |
| 打印出来没有 | 背景图不打印 | 打印预览查看 | 改 inline SVG |
| hover 不变色 | 颜色写死或用了背景图 | 检查是否 currentColor | 改 mask-image 或 inline SVG |
| 旋转动画绕远路 | 起止角度差超过 180 度 | 慢放动画观察 | 调整起止角度差 |
| 描边箭头歪斜 | transform 顺序写反 | 逐个去掉 transform 试 | 先 translate 再 rotate |
最后分享一个小习惯:我现在会在项目里建一个arrow.html的临时页面,把项目里用到的所有箭头形态(字符、CSS、SVG 各一份)并排放在一起,标上字号和颜色。每次要改箭头样式或者排查对齐问题,先在这个页面里调好再往业务代码里搬。这个页面看着简陋,但省下的时间比任何调试工具都多——毕竟箭头这种东西,眼睛在一个统一的参照系里对比,比盯着单个页面猜要靠谱得多。