Figma to React Icon Pipeline: SVGO + SVGR Automation in 2026
The complete end-to-end workflow for exporting SVG icons from Figma, optimizing with SVGO, converting to React components with SVGR, and automating the pipeline in CI.
The most common design-system bottleneck isn’t the design work — it’s the export-to-code gap. A designer finishes an icon in Figma, exports it, and a developer manually cleans the SVG, removes hardcoded colors, writes a React component, and adds it to the barrel file. Multiply that by every icon update and it becomes unsustainable.
This guide automates the entire pipeline: Figma export → SVGO optimization → SVGR component generation → barrel export → CI integration.
Pipeline overview
Figma (design source)
↓ Export SVG (with correct settings)
assets/icons/*.svg
↓ SVGO (optimize, normalize attributes)
assets/icons/*.optimized.svg
↓ SVGR (convert to React components)
src/components/icons/*.tsx
↓ Barrel file generation
src/components/icons/index.ts
↓ Import in app
import { MyIcon } from '@/components/icons';
Step 1: Export settings in Figma
Wrong export settings are the #1 source of SVG problems. Before exporting:
Frame the icons at 24×24 — each icon must be in a 24×24 Figma frame. Exporting at 32×32 or other sizes means SVGR’s viewBox assumptions break.
Export settings to use:
- Format: SVG
- “Include id attribute”: Off (ids become class conflicts in React)
- “Outline stroke”: Off (keeps paths as strokes, not filled shapes)
- “Simplify stroke”: Off (preserves stroke-linecap and stroke-linejoin)
- Contents only: On (exports just the icon content, not the surrounding frame)
Clean up before exporting:
- Remove all fills that should be
currentColor— set them to black (#000000) and SVGO will convert them - Make sure stroke width is 2px
- Flatten boolean operations that produce unnecessary path complexity
- Remove hidden layers
If “Outline stroke” is On, Figma converts your 2px stroke into a thick filled path shape. The resulting SVG is significantly more complex and the icon loses its stroke-based identity — it won’t respond to strokeWidth props.
Step 2: SVGO configuration
SVGO removes metadata, normalizes attributes, and prepares the SVG for React.
npm install --save-dev svgo
Create svgo.config.cjs in your project root:
// svgo.config.cjs
module.exports = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false, // Keep viewBox — required for scaling
inlineStyles: {
onlyMatchedOnce: false, // Convert all style attributes
},
},
},
},
'removeDimensions', // Remove width/height (use viewBox instead)
{
name: 'convertColors',
params: {
currentColor: true, // Replace #000000 with currentColor
},
},
{
name: 'removeAttrs',
params: {
attrs: ['fill', 'stroke'], // Remove hardcoded fill/stroke attributes
// (Lucide/SVGR adds these at the svg level)
},
},
],
};
Run SVGO on your exported SVGs:
# Optimize all SVGs in assets/icons/
npx svgo --folder assets/icons --output assets/icons --config svgo.config.cjs
Add to package.json:
{
"scripts": {
"icons:optimize": "svgo --folder assets/icons --config svgo.config.cjs"
}
}
After SVGO runs, open a few optimized SVGs and verify: no hardcoded colors, viewBox="0 0 24 24" present, no width/height attributes, stroke attributes removed. Any remaining hardcoded values will break dark mode in the generated components.
Step 3: SVGR component generation
SVGR converts SVG files to React components that accept standard props (size, color, className).
npm install --save-dev @svgr/cli
Create svgr.config.cjs:
// svgr.config.cjs
module.exports = {
typescript: true,
icon: true, // Sets viewBox="0 0 {em} {em}" for scalable icons
svgo: false, // We already ran SVGO separately
ref: false,
memo: false,
replaceAttrValues: {
'#000': 'currentColor', // Safety net for any missed black values
'#000000': 'currentColor',
black: 'currentColor',
},
template: (variables, { tpl }) => tpl`
import type { SVGProps } from 'react';
const ${variables.componentName} = (props: SVGProps<SVGSVGElement>) => (
${variables.jsx}
);
export default ${variables.componentName};
`,
};
Generate components:
npx svgr --config-file svgr.config.cjs --out-dir src/components/icons assets/icons/
This produces one .tsx file per SVG:
// src/components/icons/SearchIcon.tsx (generated)
import type { SVGProps } from 'react';
const SearchIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
);
export default SearchIcon;
Step 4: Barrel file generation
Instead of maintaining index.ts by hand, generate it:
npm install --save-dev barrelsby
// barrelsby.json
{
"directory": "./src/components/icons",
"delete": true,
"exclude": ["index.ts"],
"singleQuotes": true
}
npx barrelsby --config barrelsby.json
Generates:
// src/components/icons/index.ts (generated)
export { default as SearchIcon } from './SearchIcon';
export { default as SettingsIcon } from './SettingsIcon';
export { default as BellIcon } from './BellIcon';
// ...
Add to package.json scripts:
{
"scripts": {
"icons:optimize": "svgo --folder assets/icons --config svgo.config.cjs",
"icons:generate": "svgr --config-file svgr.config.cjs --out-dir src/components/icons assets/icons/",
"icons:barrel": "barrelsby --config barrelsby.json",
"icons": "npm run icons:optimize && npm run icons:generate && npm run icons:barrel"
}
}
Run the full pipeline: pnpm icons
Step 5: Integration with createLucideIcon (optional)
If you want custom icons to blend with Lucide’s API (accepting size, color, strokeWidth props exactly), wrap generated components with createLucideIcon:
// src/components/icons/CustomSearch.tsx
import { createLucideIcon } from 'lucide-react';
// Extract path data from the SVGR-generated component
const CustomSearch = createLucideIcon('CustomSearch', [
['circle', { cx: '11', cy: '11', r: '8', key: 'circle' }],
['path', { d: 'm21 21-4.35-4.35', key: 'line' }],
]);
export default CustomSearch;
This gives you full size, strokeWidth, and absoluteStrokeWidth prop support identical to built-in Lucide icons.
Step 6: CI automation
Add icon generation to your CI pipeline to catch design/code drift:
# .github/workflows/icons.yml
name: Icon Pipeline
on:
push:
paths:
- 'assets/icons/**'
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- run: pnpm install
- run: pnpm icons
- name: Check for uncommitted icon changes
run: |
git diff --exit-code src/components/icons/ || \
(echo "Generated icons differ from committed — run 'pnpm icons'" && exit 1)
This fails the CI build if anyone adds an SVG to assets/icons/ without running the generation pipeline — preventing the “icon exists in design but not in code” state.
Figma plugin automation
For teams using Figma regularly, the Figma Tokens and SVGR Figma Export plugins can trigger export directly from Figma, skipping the manual download step.
A full automated flow:
- Designer presses “Export to GitHub” in Figma plugin
- Plugin commits new SVGs to
assets/icons/via GitHub API - CI workflow triggers on the commit
- Icons pipeline runs and generates components
- PR opens automatically for developer review
Related Reading
- Creating and Exporting an Icon Library in Figma: Best Practices
- SVG Icons in Figma – Best Icon Libraries, Plugins & Import Workflow
- How to Build a Custom SVG Icon Component Library in React and Next.js
- How to Add Custom Icons to Lucide — The Complete 2026 Guide
- Managing SVG Icon Updates Across a Monorepo
- A Deep Dive into SVGO Configuration for Maximum Compression
- SVG Icon Naming Conventions for Design Systems