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.

Amit Yadav
Amit Yadav

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
Outline stroke: Off is critical

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"
  }
}
Check the output before generating components

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:

  1. Designer presses “Export to GitHub” in Figma plugin
  2. Plugin commits new SVGs to assets/icons/ via GitHub API
  3. CI workflow triggers on the commit
  4. Icons pipeline runs and generates components
  5. PR opens automatically for developer review
Share this post