React SVG Best Practices: Complete Guide to All Implementation Methods
Comprehensive guide to using SVG in React - comparing all methods including img tag, inline SVG, SVGR, icon libraries, and dynamic imports with pros, cons, and real examples
React SVG Best Practices: Complete Guide to All Implementation Methods
SVG is essential for modern React applications, but there are multiple ways to implement it - each with different tradeoffs. In this comprehensive guide, we'll explore all methods, compare their performance, and provide real-world examples to help you choose the best approach.
5 Ways to Use SVG in React
Method 1: <img> Tag
Method 2: Inline SVG
Method 3: SVGR (Component Import)
Method 4: Icon Component Libraries
Method 5: Dynamic SVG Loading
Let's explore each method in detail.
Method 1: <img> Tag
Implementation
// Simple usage
function Logo() {
return <img src="/logo.svg" alt="Company Logo" width="200" height="50" />
}
// With import
import logoUrl from './logo.svg'
function Logo() {
return <img src={logoUrl} alt="Company Logo" className="w-48 h-12" />
}
// TypeScript: add to vite-env.d.ts or globals.d.ts
declare module '*.svg' {
const content: string
export default content
}Pros
β
Simplest method - No special configuration needed
β
Browser caching - SVG files are cached separately
β
Lazy loading - Use loading="lazy" for performance
β
No bundle bloat - SVG stays as separate file
β
Familiar syntax - Works like any other image
// Lazy loading example
<img
src="/hero-illustration.svg"
alt="Hero"
loading="lazy"
decoding="async"
/>Cons
β No CSS styling - Can't change fill/stroke colors
β No JavaScript control - Can't animate individual parts
β Extra HTTP request - One more file to download
β No responsive behavior - Can't hide/show parts based on viewport
β Limited accessibility - Only alt text, no semantic structure
// β This won't work with <img> tag
<img
src="/icon.svg"
className="fill-blue-500" // Won't change SVG color
/>When to Use
β
Static logos that don't need color changes
β
Large illustrations that would bloat your bundle
β
Decorative images that don't need accessibility details
β
External SVGs from CDNs or third-party sources
Performance Example
// β
Good: Large SVG that would increase bundle size
function HeroSection() {
return (
<div className="hero">
<img
src="/hero-illustration.svg"
alt="Product illustration"
width="800"
height="600"
loading="lazy"
/>
</div>
)
}
// File size: SVG stays separate (e.g., 45 KB)
// Bundle impact: 0 KBMethod 2: Inline SVG
Implementation
function HomeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6"
>
<path d="M11.47 3.841a.75.75 0 0 1 1.06 0l8.69 8.69a.75.75 0 1 0 1.06-1.061l-8.689-8.69a2.25 2.25 0 0 0-3.182 0l-8.69 8.69a.75.75 0 1 0 1.061 1.06l8.69-8.689Z" />
<path d="m12 5.432 8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 0 1-.75-.75v-4.5a.75.75 0 0 0-.75-.75h-3a.75.75 0 0 0-.75.75V21a.75.75 0 0 1-.75.75H5.625a1.875 1.875 0 0 1-1.875-1.875v-6.198a2.29 2.29 0 0 0 .091-.086L12 5.432Z" />
</svg>
)
}Pros
β
Full CSS control - Change colors, animations, everything
β
No HTTP requests - SVG is in your bundle
β
JavaScript manipulation - Animate parts, respond to events
β
Responsive behavior - Show/hide elements based on state
β
Full accessibility - Use <title>, <desc>, ARIA attributes
β
Theme support - Use currentColor for theming
// β
Full control example
function ThemedIcon({ className }: { className?: string }) {
const [isHovered, setIsHovered] = useState(false)
return (
<svg
viewBox="0 0 24 24"
className={className}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<title>Settings</title>
<desc>Click to open settings panel</desc>
<path
fill={isHovered ? '#3B82F6' : 'currentColor'}
style={{ transition: 'fill 0.3s' }}
d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"
/>
</svg>
)
}Cons
β Bundle size increase - SVG code is in your JS bundle
β Verbose code - Large SVG paths clutter components
β Harder to maintain - Editing SVG code is tedious
β No browser caching - SVG changes when bundle changes
β Repetition - Same icon used multiple times = duplicated code
// β Bad: Large SVG bloats component
function ComplexIllustration() {
return (
<svg viewBox="0 0 500 500">
{/* 200+ lines of path data */}
<path d="M123.45 67.89 L456.78..." />
<path d="M234.56 78.90 L567.89..." />
{/* ... 198 more paths */}
</svg>
)
}
// File size: Adds 15 KB to bundle
// Maintainability: Very poorWhen to Use
β
Small icons (< 1 KB) that need styling
β
Animated SVGs that need JavaScript control
β
Frequently changing colors based on theme/state
β
Interactive SVGs with hover/click effects
β
Critical SVGs needed immediately (no loading delay)
Performance Optimization
// β
Good: Extract to separate component
const StarIcon = () => (
<svg viewBox="0 0 20 20" fill="currentColor">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
)
// Use with theming
function Rating({ stars }: { stars: number }) {
return (
<div className="flex text-yellow-400">
{Array.from({ length: stars }).map((_, i) => (
<StarIcon key={i} />
))}
</div>
)
}Method 3: SVGR (Component Import)
Setup
1. Install SVGR for Vite
npm install vite-plugin-svgr2. Configure vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr'
export default defineConfig({
plugins: [
react(),
svgr({
// SVGR options
svgrOptions: {
icon: true, // Use 1em as default size
dimensions: false, // Remove width/height attributes
typescript: true, // Generate TypeScript types
},
}),
],
})3. TypeScript types
// vite-env.d.ts
/// <reference types="vite-plugin-svgr/client" />
declare module '*.svg' {
import type { FunctionComponent, SVGProps } from 'react'
export const ReactComponent: FunctionComponent<SVGProps<SVGSVGElement>>
const src: string
export default src
}Implementation
// Import as React component
import { ReactComponent as Logo } from './logo.svg'
// Or named import
import LogoIcon from './logo.svg?react'
function Header() {
return (
<div className="header">
<Logo className="w-32 h-8 text-blue-600" />
{/* Full props support */}
<Logo
width={128}
height={32}
fill="currentColor"
aria-label="Company Logo"
/>
</div>
)
}Pros
β
Best of both worlds - File separation + full control
β
Auto-optimization - SVGR cleans SVG on import
β
TypeScript support - Full type safety with SVGProps
β
Clean code - SVG stays in separate file
β
Props support - Pass className, fill, stroke, etc.
β
Tree shaking - Unused SVGs not included in bundle
β
currentColor support - Easy theming
// β
Type-safe with autocomplete
import { ReactComponent as Icon } from './icon.svg'
function MyButton() {
return (
<button className="flex items-center gap-2">
<Icon
className="w-5 h-5"
// TypeScript shows all valid SVG props
aria-hidden="true"
focusable="false"
/>
Click me
</button>
)
}Cons
β Build configuration needed - Requires Vite/Webpack setup
β Bundle size - SVG is in bundle (like inline)
β Learning curve - Understanding import syntax and config
β Build time - SVGs processed during build
When to Use
β
Icon libraries you're building
β
Reusable components across your app
β
Type-safe projects with TypeScript
β
Modern build tools (Vite, Webpack, etc.)
β
Medium-sized SVGs (1-10 KB) needing props
Real-World Example
// icons/index.ts - Centralized icon exports
export { ReactComponent as HomeIcon } from './home.svg'
export { ReactComponent as UserIcon } from './user.svg'
export { ReactComponent as SettingsIcon } from './settings.svg'
// components/Navigation.tsx
import { HomeIcon, UserIcon, SettingsIcon } from '@/icons'
function Navigation() {
const iconClass = "w-6 h-6 transition-colors"
return (
<nav className="flex gap-4">
<a href="/" className="group">
<HomeIcon className={`${iconClass} group-hover:text-blue-500`} />
</a>
<a href="/profile" className="group">
<UserIcon className={`${iconClass} group-hover:text-blue-500`} />
</a>
<a href="/settings" className="group">
<SettingsIcon className={`${iconClass} group-hover:text-blue-500`} />
</a>
</nav>
)
}Advanced: Dual Export Pattern
// Import both ways
import logoUrl from './logo.svg' // URL string
import { ReactComponent as Logo } from './logo.svg' // Component
function App() {
return (
<>
{/* Use as <img> for static display */}
<img src={logoUrl} alt="Logo" width="200" />
{/* Use as component for styling */}
<Logo className="w-48 text-brand-500" />
</>
)
}Method 4: Icon Component Libraries
Popular Libraries
# Heroicons
npm install @heroicons/react
# Lucide
npm install lucide-react
# React Icons (aggregator - includes Font Awesome, Material, etc.)
npm install react-icons
# Phosphor Icons
npm install @phosphor-icons/reactImplementation Examples
Heroicons
import { HomeIcon, UserIcon } from '@heroicons/react/24/outline'
import { HomeIcon as HomeIconSolid } from '@heroicons/react/24/solid'
function Navigation() {
return (
<div className="flex gap-4">
{/* Outline version */}
<HomeIcon className="w-6 h-6 text-gray-600" />
{/* Solid version */}
<HomeIconSolid className="w-6 h-6 text-blue-500" />
{/* Mini version (16x16) */}
<UserIcon className="w-4 h-4" />
</div>
)
}Lucide React
import { Heart, Star, ShoppingCart } from 'lucide-react'
function ProductCard() {
return (
<div className="card">
<Heart
size={24}
color="red"
fill="pink"
strokeWidth={1.5}
/>
<Star size={20} fill="gold" stroke="orange" />
{/* Absolute size control */}
<ShoppingCart
size={32}
className="cursor-pointer hover:text-blue-500"
onClick={() => console.log('Add to cart')}
/>
</div>
)
}React Icons (Universal)
// Mix icons from different libraries
import { FaGithub, FaTwitter } from 'react-icons/fa' // Font Awesome
import { MdEmail } from 'react-icons/md' // Material Design
import { IoLogoReact } from 'react-icons/io5' // Ionicons
function SocialLinks() {
return (
<div className="flex gap-3">
<FaGithub size={24} />
<FaTwitter size={24} className="text-blue-400" />
<MdEmail size={24} />
<IoLogoReact size={24} className="text-cyan-500" />
</div>
)
}Phosphor Icons (Multiple Weights)
import { Bell } from '@phosphor-icons/react'
function Notifications() {
const [hasNotification, setHasNotification] = useState(true)
return (
<button className="relative">
<Bell
size={24}
weight={hasNotification ? 'fill' : 'regular'}
color={hasNotification ? '#3B82F6' : 'currentColor'}
/>
{/* Available weights: thin, light, regular, bold, fill, duotone */}
<Bell size={24} weight="thin" />
<Bell size={24} weight="bold" />
<Bell size={24} weight="duotone" />
</button>
)
}Pros
β
Zero configuration - Just install and use
β
Consistent design - All icons match
β
TypeScript support - Full type safety
β
Props API - size, color, className, etc.
β
Tree shaking - Only imported icons in bundle
β
Regular updates - New icons added frequently
β
Documentation - Searchable icon galleries
Cons
β Bundle size - Each icon adds to bundle
β Dependency - External package to maintain
β Limited customization - Can't edit icon paths
β Style constraints - Locked to library's design
Bundle Size Comparison
// Example bundle impact (gzipped)
import { HomeIcon } from '@heroicons/react/24/outline' // +1.2 KB
import { Heart, Star, User } from 'lucide-react' // +3.5 KB (3 icons)
import { FaGithub } from 'react-icons/fa' // +800 bytes
// β
Good: Import only what you need
import { HomeIcon } from '@heroicons/react/24/outline'
// β Bad: Import entire library
import * as Icons from '@heroicons/react/24/outline' // +85 KB!When to Use
β
Rapid prototyping - Get started quickly
β
Consistent design system - Need matching icons
β
Common UI patterns - Standard icons (home, user, settings)
β
Don't want to manage icons - Let library handle updates
β
TypeScript projects - Want full type safety
Performance Optimization
// β
Lazy load icon groups
import { lazy, Suspense } from 'react'
const SocialIcons = lazy(() => import('./SocialIcons'))
function Footer() {
return (
<Suspense fallback={<div className="h-8 w-32 bg-gray-200 animate-pulse" />}>
<SocialIcons />
</Suspense>
)
}
// SocialIcons.tsx
import { FaGithub, FaTwitter, FaLinkedin } from 'react-icons/fa'
export default function SocialIcons() {
return (
<div className="flex gap-3">
<FaGithub size={24} />
<FaTwitter size={24} />
<FaLinkedin size={24} />
</div>
)
}Method 5: Dynamic SVG Loading
Implementation
import { useState, useEffect } from 'react'
function DynamicSVG({ name }: { name: string }) {
const [svg, setSvg] = useState<string>('')
useEffect(() => {
// Dynamically import SVG
import(`./icons/${name}.svg?raw`)
.then(module => setSvg(module.default))
.catch(err => console.error('Failed to load SVG:', err))
}, [name])
return <div dangerouslySetInnerHTML={{ __html: svg }} />
}
// Usage
function App() {
return <DynamicSVG name="home" />
}Advanced: Fetch + Cache
const svgCache = new Map<string, string>()
function useSVG(url: string) {
const [svg, setSvg] = useState<string>('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
// Check cache first
if (svgCache.has(url)) {
setSvg(svgCache.get(url)!)
setLoading(false)
return
}
// Fetch SVG
fetch(url)
.then(res => res.text())
.then(text => {
svgCache.set(url, text)
setSvg(text)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [url])
return { svg, loading, error }
}
// Usage
function DynamicIcon({ src }: { src: string }) {
const { svg, loading, error } = useSVG(src)
if (loading) return <div className="animate-pulse bg-gray-200 w-6 h-6 rounded" />
if (error) return <div>Error loading icon</div>
return <div dangerouslySetInnerHTML={{ __html: svg }} />
}Pros
β
Flexible loading - Load icons on demand
β
Reduce initial bundle - Icons loaded when needed
β
Dynamic content - Load different icons based on data
β
Caching - Can implement custom cache strategy
Cons
β Complexity - More code to maintain
β Type safety - Harder to type-check
β Loading states - Need to handle loading/error
β XSS risk - Using dangerouslySetInnerHTML
β Extra requests - Network overhead
When to Use
β
CMS-driven content - Icons come from API
β
Plugin systems - User-uploaded icons
β
Very large icon sets - Can't bundle everything
β
Conditional rendering - Only load if needed
Comparison Table
| Method | Bundle Size | Styling | TypeScript | Caching | Setup | Best For |
|---|---|---|---|---|---|---|
<img> | β None | β No | β οΈ Limited | β Yes | β Easy | Static logos, large files |
| Inline SVG | β Increases | β Full | β Yes | β No | β Easy | Small interactive icons |
| SVGR | β Increases | β Full | β Full | β No | β οΈ Config needed | Reusable components |
| Icon Libraries | β οΈ Per icon | β Good | β Full | β No | β Easy | Rapid development |
| Dynamic Loading | β Minimal | β οΈ Limited | β Hard | β Custom | β Complex | CMS content |
Recommendations by Use Case
Small Project / Landing Page
Use: Icon library (Heroicons or Lucide)
import { HomeIcon, UserIcon, Cog6ToothIcon } from '@heroicons/react/24/outline'
// Simple, fast, no configuration neededDesign System / Component Library
Use: SVGR for custom icons
// Centralized, type-safe, reusable
export { ReactComponent as BrandIcon } from './brand.svg'
export { ReactComponent as CustomIcon } from './custom.svg'Large Application
Use: Combination
// Icons: Library (Heroicons)
import { HomeIcon } from '@heroicons/react/24/outline'
// Logo: <img> tag (cached separately)
<img src="/logo.svg" alt="Logo" />
// Custom illustrations: SVGR
import { ReactComponent as Hero } from './hero-illustration.svg'Performance-Critical App
Use: <img> tag + lazy loading
<img
src="/illustration.svg"
loading="lazy"
decoding="async"
/>TypeScript + Strict Type Safety
Use: SVGR or Icon Libraries
import { ReactComponent as Icon } from './icon.svg'
import { HomeIcon } from 'lucide-react'
// Both have full TypeScript supportPerformance Optimization Tips
1. Lazy Load Icons
// Route-based code splitting
const DashboardIcons = lazy(() => import('./icons/dashboard'))
const SettingsIcons = lazy(() => import('./icons/settings'))2. Use SVG Sprites
// sprite.svg
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="home" viewBox="0 0 24 24">
<path d="..." />
</symbol>
<symbol id="user" viewBox="0 0 24 24">
<path d="..." />
</symbol>
</svg>
// Usage
<svg className="w-6 h-6">
<use href="/sprite.svg#home" />
</svg>3. Optimize SVG Files
# Use Tiny SVG or SVGO
npx svgo icon.svg -o icon.optimized.svg
# Before: 2.4 KB
# After: 800 bytes4. Tree Shaking
// β
Good: Named imports (tree-shakeable)
import { HomeIcon } from '@heroicons/react/24/outline'
// β Bad: Namespace import
import * as Icons from '@heroicons/react/24/outline'Accessibility Best Practices
1. Decorative Icons
// Icon next to text - hide from screen readers
<button>
<HomeIcon aria-hidden="true" className="w-5 h-5" />
<span>Home</span>
</button>2. Standalone Icons
// Icon without text - add label
<button aria-label="Go to home page">
<HomeIcon className="w-6 h-6" />
</button>
// Or with title
<svg role="img" aria-labelledby="home-title">
<title id="home-title">Home</title>
<path d="..." />
</svg>3. Interactive Icons
// Make sure it's focusable and has role
<svg
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => e.key === 'Enter' && handleClick()}
aria-label="Delete item"
>
<path d="..." />
</svg>Conclusion
Quick Decision Guide
Choose <img> if:
- β Large SVG files (> 10 KB)
- β Don't need color/style changes
- β Static logos or illustrations
Choose Inline SVG if:
- β Very small icons (< 500 bytes)
- β Need CSS animations
- β One-off custom icons
Choose SVGR if:
- β Building a design system
- β Need TypeScript support
- β Reusable custom icons
Choose Icon Libraries if:
- β Rapid development
- β Standard UI icons
- β Don't want to manage icons
Choose Dynamic Loading if:
- β CMS-driven content
- β Very large icon collections
- β Icons from API/database
Final Recommendation
For most modern React applications in 2025:
- Icon Library (Heroicons/Lucide) for standard UI icons
- SVGR for custom brand icons and illustrations
<img>tag for large decorative SVGs
This combination provides the best balance of developer experience, performance, and flexibility.
Happy coding!