Skip to content
Axialis Developer Network

Guide

Exporting icons to React, Vue, SwiftUI & WPF (and CI)

Turn a clean SVG library into framework-native icon components for Vue 3, React, SwiftUI, and WPF — then automate the whole pipeline in CI.

Updated

  • #svg
  • #vue3
  • #react
  • #swiftui
  • #wpf
  • #icon-pipeline
  • #ci

A cleaned, optimized SVG is the starting point, not the deliverable. Every platform your icon needs to live on — a Vue 3 component library, a React design system, a SwiftUI app, a WPF desktop application — consumes that SVG differently. This guide covers how to author each target format correctly, what the common mistakes look like in each ecosystem, and how to drive the whole process from a single source in CI so you never manually convert an icon again.

If you have not cleaned your SVGs yet, start with cleaning SVGs before shipping — the patterns here assume your source files have no editor cruft, no hardcoded colors, and consistent viewBox="0 0 24 24" geometry.

The source-to-target model

The simplest mental model: one canonical SVG per icon, version-controlled, is the single source of truth. Every platform format is a generated artifact. Nothing in the components/, Assets.xcassets/, or Icons.xaml files is hand-authored; everything is produced by a build step from the SVG source.

This means:

  • Updating an icon is a one-file change to the SVG source.
  • CI regenerates all targets automatically.
  • Design QA happens on SVGs, not on twelve copies of the same glyph in different formats.
Target Format Authoring style Key file
Vue 3 .vue SFC <script setup> component ArrowRightIcon.vue
React .tsx Typed function component ArrowRightIcon.tsx
Web Component .js customElements.define arrow-right-icon.js
SwiftUI Asset catalog PNG @1x/@2x/@3x in .xcassets ArrowRight.imageset
WPF / WinUI .xaml ResourceDictionary with Path Icons.xaml

Vue 3 — the primary web target

On AxDN and on any Vue 3 project, icon components should be Vue Single File Components using <script setup>. The SVG markup lives directly in the template, making the glyph available to the Vue compiler for tree-shaking and enabling prop-driven customization without any runtime parsing.

A well-formed Vue icon SFC looks like this:

<!-- ArrowRightIcon.vue -->
<script setup lang="ts">
withDefaults(defineProps<{
  size?: number | string
  ariaLabel?: string
}>(), {
  size: 24,
  ariaLabel: undefined,
})
</script>

<template>
  <svg
    :width="size"
    :height="size"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    :aria-hidden="ariaLabel === undefined ? 'true' : undefined"
    :aria-label="ariaLabel"
    :role="ariaLabel !== undefined ? 'img' : undefined"
  >
    <path d="M5 12h14M13 6l6 6-6 6" />
  </svg>
</template>

Why this shape works

The size prop drives both width and height from a single value, which keeps the component predictable in any layout context. Omitting the prop gives a sensible 24 px default without a hardcoded attribute in the template.

Accessibility is handled at the component level: when ariaLabel is absent, the icon is assumed to be decorative and is hidden from assistive technology via aria-hidden="true". When a label is provided — for a standalone icon button, for example — the component sets role="img" and surfaces the label.

Because the stroke is currentColor, every CSS theming pattern that already exists in the parent app works without modification. See themeable icons and dark-mode variants for the full CSS custom-property setup.

Usage at the callsite is clean:

<!-- Decorative usage (icon beside text) -->
<ArrowRightIcon />

<!-- Semantic usage (icon-only button) -->
<button aria-label="Next page">
  <ArrowRightIcon :size="20" aria-label="Next page" />
</button>

Generating Vue components from SVG in bulk

Hand-writing a component for every icon in a set of 200 is not realistic. The pattern that scales is a Node.js generator script — or a dedicated tool — that reads each SVG file, extracts the inner markup, and emits a .vue SFC from a template string.

A minimal generator that covers 90% of cases:

// scripts/generate-vue-icons.js
import { readFileSync, writeFileSync, readdirSync } from 'fs'
import { join, basename } from 'path'

const SRC = 'src/icons'
const OUT = 'src/components/icons'

function toPascalCase(filename) {
  return filename
    .replace(/\.svg$/, '')
    .replace(/(^|[-_])([a-z])/g, (_, __, c) => c.toUpperCase())
    + 'Icon'
}

function extractInner(svgString) {
  return svgString
    .replace(/^[\s\S]*?<svg[^>]*>/, '')
    .replace(/<\/svg>\s*$/, '')
    .trim()
}

for (const file of readdirSync(SRC).filter(f => f.endsWith('.svg'))) {
  const name = toPascalCase(basename(file))
  const svg = readFileSync(join(SRC, file), 'utf8')
  const inner = extractInner(svg)

  const sfc = `<script setup lang="ts">
withDefaults(defineProps<{ size?: number | string; ariaLabel?: string }>(), {
  size: 24,
  ariaLabel: undefined,
})
</script>

<template>
  <svg
    :width="size"
    :height="size"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    :aria-hidden="ariaLabel === undefined ? 'true' : undefined"
    :aria-label="ariaLabel"
    :role="ariaLabel !== undefined ? 'img' : undefined"
  >
    ${inner}
  </svg>
</template>
`
  writeFileSync(join(OUT, `${name}.vue`), sfc)
}

Run this as part of your build before TypeScript compilation and your components are always in sync with the SVG source.


React — one target among several

If your organization also ships a React design system alongside a Vue product, you want to generate React components from the same SVG source, not maintain a separate library. The React component shape is deliberately parallel to the Vue one so the generator template is easy to adapt.

// ArrowRightIcon.tsx
import type { SVGProps } from 'react'

interface ArrowRightIconProps extends SVGProps<SVGSVGElement> {
  size?: number | string
}

export function ArrowRightIcon({ size = 24, 'aria-label': ariaLabel, ...props }: ArrowRightIconProps) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden={ariaLabel === undefined ? true : undefined}
      aria-label={ariaLabel}
      role={ariaLabel !== undefined ? 'img' : undefined}
      {...props}
    >
      <path d="M5 12h14M13 6l6 6-6 6" />
    </svg>
  )
}

Extending SVGProps means consumers can pass any native SVG attribute without the component needing to explicitly declare it, which keeps the API surface small.


Web Components — framework-free icon delivery

When icons need to work in a mixed-framework environment — a documentation site, an embedded widget, or a Lit/Stencil component library — a Web Component wrapping the SVG is the right unit. It works without Vue, React, or any build tool in the consumer's project.

// arrow-right-icon.js
class ArrowRightIcon extends HTMLElement {
  static observedAttributes = ['size', 'aria-label']

  connectedCallback() { this.render() }
  attributeChangedCallback() { this.render() }

  render() {
    const size = this.getAttribute('size') || '24'
    const label = this.getAttribute('aria-label')
    this.innerHTML = `
      <svg width="${size}" height="${size}" viewBox="0 0 24 24"
           fill="none" stroke="currentColor" stroke-width="2"
           stroke-linecap="round" stroke-linejoin="round"
           ${label ? `role="img" aria-label="${label}"` : 'aria-hidden="true"'}>
        <path d="M5 12h14M13 6l6 6-6 6"/>
      </svg>`
  }
}

customElements.define('arrow-right-icon', ArrowRightIcon)

SwiftUI — SF Symbols first, asset catalog second

Before reaching for a custom icon in a SwiftUI project, check whether the SF Symbols library covers it. SF Symbols are vector glyphs built into Apple platforms (available from iOS 13 / macOS 11 onwards), and they adapt automatically to Dynamic Type sizes, weight settings, and system color environments including Dark Mode.

// Prefer this when the symbol exists
Image(systemName: "arrow.right")
    .imageScale(.medium)
    .foregroundStyle(.primary)

When you need a custom glyph — a brand icon, a product-specific UI element, an illustration style that does not match the San Francisco system look — use an asset catalog image set. Export your SVG as PNG rasters at three sizes: @1x (24 × 24 px), @2x (48 × 48 px), and @3x (72 × 72 px). Place them in an .imageset inside Assets.xcassets with a Contents.json that declares all three scales, then reference them by name:

// Custom asset from the image catalog
Image("ArrowRight")
    .resizable()
    .scaledToFit()
    .frame(width: 24, height: 24)
    .foregroundStyle(.primary)

A few things that trip developers up with custom SwiftUI icons:

  • Asset catalog images do not automatically scale with Dynamic Type the way SF Symbols do. If your icon should grow when the user increases their text size, wrap the frame in a ScaledMetric calculation.
  • Template rendering mode (Image("ArrowRight").renderingMode(.template)) replaces all non-transparent pixels with foregroundStyle, which is the mechanism that gives you currentColor-like behavior for single-color icons.
  • For icons that appear in both light and dark appearances, add both an "Any Appearance" and a "Dark" slot in the imageset, or design the icon to read well on both surfaces (which is usually possible for line-art style glyphs).

The output from this step — a properly populated .xcassets folder — is exactly what packaging and final delivery picks up for app bundle assembly.


WPF — XAML resource dictionaries

WPF does not natively consume SVG. The standard production pattern is to convert your icon paths into a ResourceDictionary of Path or DrawingImage elements, stored in an Icons.xaml file that the application merges at startup.

A ResourceDictionary approach keeps every icon available by key, styled by the surrounding theme, and infinitely scalable because the geometry is stored as path data — not rasterized.

<!-- Icons.xaml — ResourceDictionary excerpt -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <!-- Store just the geometry; apply fill and size at the callsite -->
  <Geometry x:Key="ArrowRightGeometry">M5 12h14M13 6l6 6-6 6</Geometry>

  <!-- Or wrap as a DrawingImage for use as an ImageSource -->
  <DrawingImage x:Key="ArrowRightIcon">
    <DrawingImage.Drawing>
      <GeometryDrawing Brush="{DynamicResource IconForegroundBrush}">
        <GeometryDrawing.Geometry>
          <PathGeometry Figures="M5 12h14M13 6l6 6-6 6"/>
        </GeometryDrawing.Geometry>
      </GeometryDrawing>
    </DrawingImage.Drawing>
  </DrawingImage>

</ResourceDictionary>

Merge the dictionary at the application level so it is available everywhere:

<!-- App.xaml -->
<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="Styles/Icons.xaml"/>
    </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>

Using the icon in a button:

<Button ToolTip="Next page">
  <Path Data="{StaticResource ArrowRightGeometry}"
        Fill="{DynamicResource IconForegroundBrush}"
        Width="16" Height="16" Stretch="Uniform"/>
</Button>

The key decision is {StaticResource} vs {DynamicResource}: use DynamicResource for any brush that needs to update when the user switches themes at runtime. StaticResource is resolved once at load time and is slightly faster, but will not update on a theme change.

Converting SVG path data to XAML

SVG path syntax and XAML Path.Data syntax are very similar but not identical. SVG uses the full d attribute spec (including shorthand arc commands); XAML PathGeometry accepts the same syntax via Figures. For most stroke-based icons the path data transfers directly. Watch out for:

  • Stroke rendering: SVG strokes are rendered by the browser's stroke engine; XAML Path also has a StrokeThickness property. Use it instead of baking stroke width into the geometry.
  • fill-rule: SVG's evenodd maps to FillRule="EvenOdd" on PathGeometry.
  • Relative vs absolute commands: both SVG and XAML support both; make sure your generator preserves whichever form the source uses.

Generating all targets in CI

The full pipeline — one SVG source generates Vue SFCs, React TSX, and XAML entries — is a build script running in your CI environment. Here is a GitHub Actions workflow that wires it together:

# .github/workflows/generate-icons.yml
name: Generate icon components

on:
  push:
    paths:
      - 'src/icons/**'

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Clean and optimize SVGs
        run: npx svgo --config svgo.config.js --folder src/icons --output src/icons

      - name: Generate Vue 3 components
        run: node scripts/generate-vue-icons.js

      - name: Generate React components
        run: node scripts/generate-react-icons.js

      - name: Generate XAML resource dictionary
        run: node scripts/generate-xaml-icons.js

      - name: Commit generated files
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "chore: regenerate icon components from SVG source"
          file_pattern: |
            src/components/icons/**
            src/xaml/Icons.xaml

The workflow triggers only when files under src/icons/ change, so it does not run on unrelated commits. The SVGO optimization step runs before generation, ensuring that hand-edited SVG typos or cruft from a designer's export never make it into component code.

For the SwiftUI pipeline, the asset catalog population step is typically part of the Xcode build process or a separate script that runs on a macOS runner — generating PNG rasters from SVG source using a tool like rsvg-convert (part of librsvg), then assembling the Contents.json for each imageset.

The end state is a repository where no icon component file is ever hand-edited. The source of truth is the SVG, and the generated artifacts are clearly marked as such.

For the next step once icons are exported, packaged, and ready — how to assemble them into a distributable product — see packaging and final delivery.