A designer on my team once spent an entire afternoon manually exporting 48 icons as individual SVGs because our "icon system" was just a Figma file named "Icons - FINAL v3." Each icon was a different artboard size, some had strokes and some had fills, and the file names were things like "icon-arrow-right-UPDATED." When the frontend developer tried to use them, half the icons clipped at the viewBox boundary. I've been there. Here's the system I use now that stops this from happening.
All icons live in one Figma file, on a grid of 24×24 artboards. Every icon uses the same base grid, same stroke width (1.5px), and same naming convention: icon-name--variant. No "FINAL" or "UPDATED" suffixes. The file is linked to the design system documentation in Notion or Storybook, not buried in someone's Drafts folder.
From that source file, icons are exported via a Figma plugin (SVG Export) with these settings checked: Inline SVG, Remove unused IDs, Minify output.
For sites that aren't using a component framework, I create an SVG sprite sheet — a single file containing all icons as <symbol> elements, each with a unique ID. You include it once in the page (hidden) and reference individual icons with <use>:
<!-- Include once, usually in the header -->
<svg style="display:none" aria-hidden="true">
<symbol id="icon-search" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16...">
</symbol>
<symbol id="icon-cart" viewBox="0 0 24 24">
<path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7...">
</symbol>
</svg>
<!-- Use anywhere on the page -->
<button aria-label="Search">
<svg aria-hidden="true"><use href="#icon-search"/></svg>
</button>
The advantage: one HTTP request for all icons. The browser caches the sprite sheet, and every icon reference is instant. The viewBox on each <symbol> ensures scaling works correctly regardless of the <svg> container size.
In a component-based project, each icon becomes its own component. I use SVGR (React) or the equivalent for Vue/Svelte to auto-generate components from the exported SVG files. The build script runs on every push, so icons in production always match the Figma source.
// IconSearch.tsx — auto-generated from icon-search.svg
import { SVGProps } from 'react';
export const IconSearch = (props: SVGProps<SVGSVGElement>) => (
<svg width={24} height={24} fill="none" {...props}>
<path d="M15.5 14h-.79l-.28-.27..." />
</svg>
);
The component accepts standard SVG props — className, color, size — so the consuming developer never needs to open the SVG file. They just import and use it like any other component.
Every bad icon system I've inherited had the same problems:
stroke for drawing can be recolored with CSS. Icons that use fill can too, but mixing both in the same library means some icons change color on hover and some don't.Fixing this after the fact is tedious but worth it. I usually spend a day standardizing everything to 24×24, stroke-only (with fill="none"), and renaming files before generating components. The one-time cost pays off every time someone adds an icon without asking me.
The whole setup takes about 2 hours for a new project and saves dozens of hours over its lifetime. More importantly, it means nobody ever has to ask "where's the icon file?" — the answer is always the same place.