jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

SVG from Zero to Senior · Part 17 — The Icon Sprite Build Pipeline

Industrialize icons: turn a folder of .svg files into one cached <symbol> sprite with SVGO + svg-sprite, then use it via <use href>. External vs inline sprites, caching, currentColor theming, and framework integration. With a live demo.

You met <symbol> + <use> in Part 10. {Con đã gặp <symbol> + <use> ở Phần 10.} Now we make it industrial: you don’t hand-write a sprite, you generate it from a folder of icon files in your build. {Giờ ta làm nó công nghiệp: con không viết sprite bằng tay, mà sinh nó từ một thư mục file icon trong build.} This is how every real design system ships hundreds of icons as one tiny, cacheable file. {Đây là cách mọi design system thật ship hàng trăm icon thành một file nhỏ, cache được.}

Watch five files collapse into one sheet, then theme and resize the result. {Xem năm file gộp thành một sheet, rồi đổi theme và đổi cỡ kết quả.}

Open the full demo {Mở demo đầy đủ}: /tools/svg-sprite-demo/.

Why a sprite at all? {Vì sao cần sprite?}

Imagine 80 icons shipped as 80 .svg files: {Hình dung 80 icon ship thành 80 file .svg:}

  • 80 HTTP requests (or 80 inlined copies bloating your HTML/JS bundle). {80 request HTTP (hoặc 80 bản inline làm phình bundle).}
  • 80 copies of <svg xmlns…> boilerplate. {80 bản boilerplate <svg xmlns…>.}

A sprite is one file containing one <symbol id> per icon. {Sprite là một file chứa một <symbol id> cho mỗi icon.} You fetch it once, the browser caches it, and you stamp icons with <use>. {Con fetch nó một lần, trình duyệt cache, và con đóng dấu icon bằng <use>.} Boilerplate is written once; only the unique paths repeat. {Boilerplate viết một lần; chỉ path riêng lặp lại.}

The build step {Bước build}

The standard pipeline is two tools: SVGO (optimize each icon, Part 10) then svg-sprite (merge into a <symbol> sheet). {Pipeline chuẩn là hai công cụ: SVGO (tối ưu mỗi icon) rồi svg-sprite (gộp thành sheet <symbol>).}

# optimize every icon, then build the symbol sprite
npx svgo -f ./icons -o ./icons-min
npx svg-sprite --symbol --symbol-dest=dist --symbol-sprite=sprite.svg ./icons-min/*.svg

The output is one file: {Đầu ra là một file:}

<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
  <symbol id="ic-home" viewBox="0 0 24 24"><path d="M3 11l9-8 9 8M5 10v10h14V10"/></symbol>
  <symbol id="ic-search" viewBox="0 0 24 24">…</symbol>
  <!-- one <symbol> per source file, id derived from the filename -->
</svg>

In a Vite/Next/Astro project you’d use a plugin instead of CLI calls — vite-plugin-svg-icons, vite-svg-sprite-wrapper, or SVGR’s sprite mode — so the sprite rebuilds on save and gets hashed for cache-busting. {Trong dự án Vite/Next/Astro con dùng plugin thay vì gọi CLI — vite-plugin-svg-icons, vite-svg-sprite-wrapper, hoặc chế độ sprite của SVGR — để sprite tự build lại khi lưu và được hash để bẻ cache.}

Two ways to deliver the sprite {Hai cách giao sprite}

External file (best for caching) {File ngoài (tốt nhất cho cache)}

Reference a symbol in a separate .svg file by URL + fragment: {Tham chiếu một symbol trong file .svg riêng bằng URL + fragment:}

<svg class="icon"><use href="/sprite.svg#ic-home" /></svg>

The sprite is cached once and shared across every page — the most efficient option for multi-page sites. {Sprite được cache một lần và dùng chung mọi trang — lựa chọn hiệu quả nhất cho site nhiều trang.} (Old IE needed an svg4everybody polyfill for external <use>; modern browsers don’t.) {(IE cũ cần polyfill svg4everybody cho <use> ngoài; trình duyệt hiện đại thì không.)}

Inline injection (zero extra requests) {Inject inline (không request thêm)}

Drop the whole sprite (with display:none) at the top of <body>, then reference local fragments: {Thả cả sprite (với display:none) ở đầu <body>, rồi tham chiếu fragment cục bộ:}

<body>
  <svg style="display:none"> … all symbols … </svg>
  <svg class="icon"><use href="#ic-home" /></svg>
</body>

No extra request, but the sprite re-downloads with every page (no separate caching). {Không request thêm, nhưng sprite tải lại mỗi trang (không cache riêng).} Senior call: external file for multi-page/SSR sites; inline injection for SPAs where it loads once. {Quyết định senior: file ngoài cho site nhiều trang/SSR; inject inline cho SPA tải một lần.}

Theming with currentColor {Đổi theme bằng currentColor}

Author the source icons with stroke="currentColor" / fill="currentColor" (Part 10 again). {Viết icon nguồn với stroke="currentColor" / fill="currentColor".} Then one CSS rule sizes and colors them all: {Rồi một luật CSS đổi cỡ và màu tất cả:}

.icon { width: 1.25em; height: 1.25em; color: inherit; }   /* follows text */
.btn-danger .icon { color: #ef4444; }

This is the reason a sprite icon system feels effortless — sizing follows font-size via em, color follows currentColor, no per-icon CSS. {Đây là lý do hệ icon sprite cảm giác nhẹ tênh — cỡ theo font-size qua em, màu theo currentColor, không CSS từng icon.}

A typed helper component {Một component helper có kiểu}

In a framework, wrap <use> so consumers never touch raw markup: {Trong framework, bọc <use> để người dùng không động markup thô:}

type IconName = 'home' | 'search' | 'bell' | 'user' | 'gear';

export function Icon({ name, size = 24 }: { name: IconName; size?: number }) {
  return (
    <svg width={size} height={size} aria-hidden="true">
      <use href={`/sprite.svg#ic-${name}`} />
    </svg>
  );
}

The IconName union gives you autocomplete and a compile error for typos — a small touch that scales a design system. {Union IconName cho con autocomplete và lỗi biên dịch khi gõ sai — một chạm nhỏ giúp design system mở rộng.}

The master’s warnings {Lời cảnh báo của sư phụ}

  • Don’t let SVGO drop viewBox in the build — symbols without it won’t scale. {Đừng để SVGO bỏ viewBox trong build — symbol thiếu nó không scale được.}
  • Unique, prefixed ids (ic-*) so symbols never clash with gradient/filter ids elsewhere. {Id duy nhất, có tiền tố (ic-*) để symbol không đụng id gradient/filter chỗ khác.}
  • External <use href> needs same-origin (or CORS). Cross-origin sprite fragments are blocked. {<use href> ngoài cần cùng origin (hoặc CORS). Fragment sprite khác origin bị chặn.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. Build a sprite {Dựng một sprite}: take 5 icons, run SVGO + svg-sprite, and reference them via <use href="sprite.svg#…">. {lấy 5 icon, chạy SVGO + svg-sprite, và tham chiếu qua <use>.}
  2. Theme test {Kiểm tra theme}: author them with currentColor and re-color a group with one parent color. {viết chúng với currentColor và đổi màu cả nhóm bằng một color cha.}
  3. Typed <Icon> {<Icon> có kiểu}: build the union-typed wrapper component and confirm a typo is a compile error. {dựng component bọc có union type và xác nhận gõ sai là lỗi biên dịch.}

What’s next {Phần tiếp theo}

Your icons now ship like a pro pipeline. {Icon của con giờ ship như pipeline chuyên nghiệp.} For motion at scale, hand-rolled rAF isn’t always enough. {Cho chuyển động ở quy mô, rAF tự code không phải lúc nào cũng đủ.} In Part 18 we survey the animation libraries for SVG — GSAP, anime.js, and Motion — what each is best at, the timeline/orchestration features you’d otherwise rebuild, and when plain CSS/SMIL still wins. {Ở Phần 18 ta khảo sát các thư viện animation cho SVG — GSAP, anime.js, và Motion — cái nào mạnh gì, các tính năng timeline/điều phối con sẽ phải dựng lại, và khi nào CSS/SMIL thuần vẫn thắng.}