前置知识: SVG

SVG 图标系统工程

4 min中级

图标设计原则(统一画布、currentColor、像素网格)、sprite 实现与尺寸颜色变体、命名规范与构建链路。

前置知识

建议先阅读以下内容再进入本文:


前置知识:symbol / use 复用机制(《SVG 符号与复用》,120-SVGSymbolReuse)与 CSS 变量穿透(《SVG CSS 样式化》,130-SVGCSSStyling)。

学习目标:掌握 currentColor 主题化与像素网格对齐两条图标设计铁律;会搭一套从源文件到 sprite 的图标工程链路(含构建脚本与 TS 声明);知道图标按钮、状态图标这类工程组件如何承接无障碍属性(属性语义本身见《SVG 可访问性》)。

无障碍属性的判定规则(装饰/语义/交互三分法、ARIA 速查、键盘导航、reduced-motion、对比度)已独立成篇,见 SVG 可访问性;本文专注图标作为工程资产的另一半:怎么画、怎么管、怎么建。

1. 为什么用 SVG 图标

维度SVG 图标字体图标(如 Font Awesome)PNG 图标
缩放无损无损锯齿
颜色CSS 控制CSS 控制(有限)固定
可访问性原生支持一般需 alt
文件体积小(单图标)中(整包)大
动画支持有限不支持
语义化DOM 节点字符图片

SVG 是现代 Web 图标的首选方案。

2. 图标设计原则

2.1 统一画布

所有图标使用相同 viewBox(通常 24×24):

html
<symbol id="icon-home" viewBox="0 0 24 24">...</symbol>
<symbol id="icon-search" viewBox="0 0 24 24">...</symbol>

2.2 描边一致

html
<symbol id="icon-home" viewBox="0 0 24 24">
  <path
    d="..."
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
  />
</symbol>

统一描边宽度、端点、拐角,保持视觉一致性。

2.3 使用 currentColor

html
<!-- 错误:硬编码颜色 -->
<symbol id="icon-home">
  <path fill="#4f5bd5" />
</symbol>

<!-- 正确:使用 currentColor -->
<symbol id="icon-home">
  <path fill="currentColor" />
</symbol>

currentColor 让图标颜色继承父元素 color,实现主题化。

2.4 对齐像素网格

html
<!-- 1:1 渲染时锐利:1px 描边落在半像素坐标(与像素边界重合) -->
<path d="M 0.5 0.5 L 10.5 0.5" stroke-width="1" />

<!-- 2px 及以上描边、填充形状边缘:用整数坐标 -->
<path d="M 0 0 L 10 0" stroke-width="2" />

细描边图标需关注像素网格:1px 描边在 1:1 渲染下落在半像素坐标最锐利,填充边缘则优先整数坐标(原理见《SVG 坐标系统与 viewBox》的抗锯齿一节)。图标常以 16/24/32 等多种尺寸渲染,最终以目标尺寸的实际显示效果为准。

3. 图标系统实现

3.1 Sprite 模式

html
<!-- icons.svg 隐藏文件 -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
  <symbol id="icon-home" viewBox="0 0 24 24">
    <path
      d="M3 12 L12 3 L21 12 M5 10 V21 H19 V10"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </symbol>
  <symbol id="icon-search" viewBox="0 0 24 24">
    <circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
    <line
      x1="16"
      y1="16"
      x2="21"
      y2="21"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
    />
  </symbol>
  <symbol id="icon-user" viewBox="0 0 24 24">
    <circle cx="12" cy="8" r="4" fill="currentColor" />
    <path
      d="M4 20 C4 16 8 14 12 14 C16 14 20 16 20 20"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
    />
  </symbol>
</svg>

3.2 使用图标

html
<svg class="icon" aria-hidden="true">
  <use href="#icon-home" />
</svg>

调用点默认按装饰图标处理(aria-hidden);需要独立表意的图标如何改属性,见 SVG 可访问性 的三分法。

css
.icon {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

3.3 尺寸变体

css
.icon-sm {
  width: 16px;
  height: 16px;
}
.icon-md {
  width: 24px;
  height: 24px;
}
.icon-lg {
  width: 32px;
  height: 32px;
}
.icon-xl {
  width: 48px;
  height: 48px;
}
html
<svg class="icon icon-sm"><use href="#icon-home" /></svg>
<svg class="icon icon-lg"><use href="#icon-home" /></svg>

3.4 颜色变体

css
.icon-primary {
  color: #4f5bd5;
}
.icon-success {
  color: #00b894;
}
.icon-danger {
  color: #d63031;
}
.icon-warning {
  color: #f9a825;
}
html
<button class="btn">
  <svg class="icon icon-danger"><use href="#icon-delete" /></svg>
  删除
</button>

4. 图标按钮组件

html
<button class="btn-icon btn-icon-danger" aria-label="删除项目">
  <svg class="icon" aria-hidden="true" viewBox="0 0 24 24">
    <path
      d="M3 6 H21 M8 6 V4 H16 V6 M6 6 L7 20 H17 L18 6"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
</button>

<style>
  .btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 8px;
    background: transparent;
    cursor: pointer;
    transition: background 0.2s;
  }
  .btn-icon:hover {
    background: rgba(0, 0, 0, 0.05);
  }
  .btn-icon:focus-visible {
    outline: 2px solid #4f5bd5;
    outline-offset: 2px;
  }
  .btn-icon-danger {
    color: #d63031;
  }
  .btn-icon-danger:hover {
    background: rgba(214, 48, 49, 0.1);
  }
  .icon {
    width: 20px;
    height: 20px;
  }
</style>

5. 动态图标

5.1 加载状态

html
<svg class="icon icon-spin" viewBox="0 0 24 24" aria-label="加载中" role="img">
  <path
    d="M12 2 A10 10 0 0 1 22 12"
    fill="none"
    stroke="currentColor"
    stroke-width="3"
    stroke-linecap="round"
  />
</svg>

<style>
  .icon-spin {
    animation: spin 1s linear infinite;
    transform-origin: center;
    transform-box: fill-box;
  }
  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }
</style>

5.2 状态切换

html
<button class="btn-toggle" aria-pressed="false" id="like-btn">
  <svg class="icon" viewBox="0 0 24 24">
    <path
      class="heart-outline"
      d="M12 21 L4 13 C2 11 2 8 4 6 C6 4 9 4 12 7 C15 4 18 4 20 6 C22 8 22 11 20 13 Z"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
    />
    <path
      class="heart-fill"
      d="M12 21 L4 13 C2 11 2 8 4 6 C6 4 9 4 12 7 C15 4 18 4 20 6 C22 8 22 11 20 13 Z"
      fill="currentColor"
    />
  </svg>
</button>

<style>
  .btn-toggle .heart-fill {
    display: none;
  }
  .btn-toggle[aria-pressed='true'] .heart-outline {
    display: none;
  }
  .btn-toggle[aria-pressed='true'] .heart-fill {
    display: block;
  }
  .btn-toggle[aria-pressed='true'] {
    color: #d63031;
  }
</style>

<script>
  const btn = document.getElementById('like-btn');
  btn.addEventListener('click', () => {
    const pressed = btn.getAttribute('aria-pressed') === 'true';
    btn.setAttribute('aria-pressed', !pressed);
  });
</script>

aria-pressed 表示按钮按下状态,配合 CSS 切换图标。

6. 图标命名规范

plaintext
icon-{category}-{name}
命名含义
icon-action-home操作类 - 首页
icon-action-search操作类 - 搜索
icon-media-play媒体类 - 播放
icon-media-pause媒体类 - 暂停
icon-status-success状态类 - 成功
icon-status-error状态类 - 错误
icon-nav-menu导航类 - 菜单
icon-nav-close导航类 - 关闭

7. 图标集管理

7.1 目录结构

plaintext
src/
  assets/
    icons/
      action/
        home.svg
        search.svg
      media/
        play.svg
        pause.svg
      status/
        success.svg
        error.svg
  sprite/
    icons.svg       # 构建生成的 sprite
    icons.ts        # TypeScript 声明

7.2 构建脚本

javascript
// scripts/build-icons.js
const fs = require('fs');
const path = require('path');
const { optimize } = require('svgo');

const iconsDir = path.join(__dirname, '../src/assets/icons');
const outputPath = path.join(__dirname, '../src/sprite/icons.svg');

function buildSprite() {
  const symbols = [];
  function walk(dir) {
    const items = fs.readdirSync(dir);
    for (const item of items) {
      const fullPath = path.join(dir, item);
      const stat = fs.statSync(fullPath);
      if (stat.isDirectory()) {
        walk(fullPath);
      } else if (item.endsWith('.svg')) {
        const name = path.basename(item, '.svg');
        const content = fs.readFileSync(fullPath, 'utf8');
        const optimized = optimize(content, {
          plugins: [{ name: 'preset-default' }, { name: 'removeDimensions' }],
        }).data;
        // 提取内容并转为 symbol
        const inner = optimized.replace(/<svg[^>]*>|<\/svg>/g, '');
        symbols.push(`<symbol id="icon-${name}" viewBox="0 0 24 24">${inner}</symbol>`);
      }
    }
  }
  walk(iconsDir);
  const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${symbols.join('')}</svg>`;
  fs.writeFileSync(outputPath, sprite);
  console.log(`Built ${symbols.length} icons`);
}

buildSprite();

8. 实战:完整的图标按钮系统

html
<!DOCTYPE html>
<html>
  <head>
    <style>
      .icon {
        width: 24px;
        height: 24px;
        fill: none;
        stroke: currentColor;
        stroke-width: 2;
      }
      .btn {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 8px 16px;
        border: 1px solid #ddd;
        border-radius: 8px;
        background: #fff;
        color: #333;
        cursor: pointer;
        font-size: 14px;
        transition: all 0.2s;
      }
      .btn:hover {
        background: #f5f5f5;
        border-color: #ccc;
      }
      .btn:focus-visible {
        outline: 2px solid #4f5bd5;
        outline-offset: 2px;
      }
      .btn-primary {
        background: #4f5bd5;
        border-color: #4f5bd5;
        color: #fff;
      }
      .btn-primary:hover {
        background: #3a47b8;
      }
      .btn-danger {
        color: #d63031;
        border-color: #d63031;
      }
      .btn-danger:hover {
        background: #fbe9e7;
      }
      .btn-icon-only {
        padding: 8px;
      }
    </style>
  </head>
  <body>
    <svg style="display:none">
      <symbol id="icon-plus" viewBox="0 0 24 24">
        <path d="M12 5 V19 M5 12 H19" stroke-linecap="round" />
      </symbol>
      <symbol id="icon-trash" viewBox="0 0 24 24">
        <path
          d="M3 6 H21 M8 6 V4 H16 V6 M6 6 L7 20 H17 L18 6"
          stroke-linecap="round"
          stroke-linejoin="round"
        />
      </symbol>
      <symbol id="icon-check" viewBox="0 0 24 24">
        <path d="M5 12 L10 17 L19 8" stroke-linecap="round" stroke-linejoin="round" />
      </symbol>
    </svg>

    <button class="btn btn-primary">
      <svg class="icon" aria-hidden="true"><use href="#icon-plus" /></svg>
      新建项目
    </button>

    <button class="btn btn-danger btn-icon-only" aria-label="删除">
      <svg class="icon" aria-hidden="true"><use href="#icon-trash" /></svg>
    </button>

    <button class="btn" aria-pressed="false" id="check-btn">
      <svg class="icon" aria-hidden="true"><use href="#icon-check" /></svg>
      标记完成
    </button>

    <script>
      document.getElementById('check-btn').addEventListener('click', function () {
        const pressed = this.getAttribute('aria-pressed') === 'true';
        this.setAttribute('aria-pressed', !pressed);
      });
    </script>
  </body>
</html>

小结

初学者要点:

  • 图标工程的两条设计铁律:换色靠 currentColor(颜色继承父元素),换尺寸靠 CSS 尺寸类(viewBox 坐标不动),两者都不需要为每个变体重画图标。
  • sprite + <use> 是图标工程的标准交付形态:一份 sprite 文件、若干调用点,构建脚本把源 SVG 批量转 symbol。
  • 命名即文档:icon-{category}-{name} 的三段命名让几百枚图标在代码里可搜索、可审计。

进阶注意:

  • SVGO 的 cleanupIds 会砍掉 sprite 里被 <use> 引用的 id——sprite 构建链路必须关闭该插件。
  • 图标按钮与状态图标是无障碍属性的主要落点:命名放外层控件、状态走 aria-pressed 的完整语义,见《SVG 可访问性》;工程侧只要保证组件模板预留这些属性位。