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.
Con đã gặp <symbol> + <use> ở Phần 10. 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. Đây là cách mọi design system thật ship hàng trăm icon thành một file nhỏ, cache được.
Xem năm file gộp thành một sheet, rồi đổi theme và đổi cỡ kết quả.
Mở demo đầy đủ:
Vì sao cần sprite?
Hình dung 80 icon ship thành 80 file .svg:
- 80 request HTTP (hoặc 80 bản inline làm phình bundle).
- 80 bản boilerplate
<svg xmlns…>.
Sprite là một file chứa một <symbol id> cho mỗi icon. Con fetch nó một lần, trình duyệt cache, và con đóng dấu icon bằng <use>. Boilerplate viết một lần; chỉ path riêng lặp lại.
Bước build
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
Đầ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>
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.
Hai cách giao sprite
File ngoài (tốt nhất cho cache)
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>
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. (IE cũ cần polyfill svg4everybody cho <use> ngoài; trình duyệt hiện đại thì không.)
Inject inline (không request thêm)
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>
Không request thêm, nhưng sprite tải lại mỗi trang (không cache riêng). Quyết định senior: file ngoài cho site nhiều trang/SSR; inject inline cho SPA tải một lần.
Đổi theme bằng currentColor
Viết icon nguồn với stroke="currentColor" / fill="currentColor". 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; }
Đâ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.
Một component helper có kiểu
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>
);
}
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.
Lời cảnh báo của sư phụ
- Đừng để SVGO bỏ
viewBoxtrong build — symbol thiếu nó không scale được. - Id duy nhất, có tiền tố (
ic-*) để symbol không đụng id gradient/filter chỗ khác. <use href>ngoài cần cùng origin (hoặc CORS). Fragment sprite khác origin bị chặn.
Luyện tập, không thì coi như chưa học
- Dựng một sprite: lấy 5 icon, chạy SVGO + svg-sprite, và tham chiếu qua
<use>. - Kiểm tra theme: viết chúng với
currentColorvà đổi màu cả nhóm bằng mộtcolorcha. <Icon>có kiểu: dựng component bọc có union type và xác nhận gõ sai là lỗi biên dịch.
Phần tiếp theo
Icon của con giờ ship như pipeline chuyên nghiệp. Cho chuyển động ở quy mô, rAF tự code không phải lúc nào cũng đủ. Ở 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.