You have chosen your icon library, downloaded the files, and now you need to actually put icons into your project. This guide covers every practical aspect of using icons — from the simplest HTML approach to advanced framework integrations, performance optimization, accessibility compliance, and the real-world patterns used by production applications.
The 3 Ways to Add Icons to a Website
There are fundamentally three approaches to using icons on the web. Each has trade-offs that matter depending on your project.
// Method 1: Inline SVG
Inline SVG means pasting the SVG markup directly into your HTML. This gives you complete control over every aspect of the icon — color, size, stroke width, animation — using standard CSS.
<button aria-label="Search">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.3-4.3"/>
</svg>
</button>Pros: Full CSS control, no network request, renders immediately, supports currentColor for automatic color inheritance, can be animated with CSS or JavaScript.
Cons: Adds to HTML document size, cannot be cached separately from the page, verbose markup that clutters templates.
Best for: Small to medium projects, icons that need dynamic styling, critical above-the-fold icons that must render without delay.
// Method 2: External SVG via img Tag
You can reference SVG files using a standard <img> tag, treating them like any other image.
<img src="/icons/search.svg" alt="Search" width="24" height="24" />
Pros: Clean markup, SVGs are cached by the browser, easy to understand.
Cons: Cannot style with CSS (no color changes, no stroke modifications), requires a network request per icon, no currentColor support.
Best for: Static icons that never change color, content images, situations where caching matters more than styling flexibility.
// Method 3: SVG Sprite Sheet
An SVG sprite combines multiple icons into a single file. You reference individual icons using the <use> element.
<!-- Load sprite once (usually in the body) -->
<svg style="display:none">
<symbol id="icon-search" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.3-4.3"/>
</symbol>
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
</symbol>
</svg>
<!-- Use icons anywhere -->
<svg width="24" height="24"><use href="#icon-search"/></svg>
<svg width="24" height="24"><use href="#icon-home"/></svg>Pros: Single network request for all icons, supports CSS styling, keeps markup clean, excellent caching.
Cons: More complex setup, all icons load even if only some are used, harder to tree-shake.
Best for: Large projects with many icons, server-rendered sites, projects that cannot use JavaScript frameworks.
Using Icons in React and Next.js
Most modern icon libraries provide React components that make usage extremely simple.
// Installing an Icon Library
# Lucide Icons (recommended) npm install lucide-react # Heroicons npm install @heroicons/react # Phosphor Icons npm install @phosphor-icons/react # Tabler Icons npm install @tabler/icons-react
// Basic Usage
import { Search, Home, Bell, Settings } from 'lucide-react'
function Navbar() {
return (
<nav>
<a href="/"><Home size={20} /> Home</a>
<a href="/search"><Search size={20} /> Search</a>
<a href="/notifications"><Bell size={20} /> Notifications</a>
<a href="/settings"><Settings size={20} /> Settings</a>
</nav>
)
}Every icon is an individual named export. This means your bundler (Webpack, Vite, Turbopack) can tree-shake unused icons — if you import 10 icons from a library of 1,500, only those 10 are included in your production bundle.
// Customizing Icons with Props
React icon components accept props to control their appearance:
<Search
size={32} // Width and height in pixels
color="#6366f1" // Stroke/fill color
strokeWidth={1.5} // Line thickness (outline icons)
className="my-icon" // CSS class for additional styling
/>// Using currentColor for Theme-Aware Icons
The most powerful technique for icon styling is currentColor. When an SVG uses stroke="currentColor" or fill="currentColor", it automatically inherits the text color of its parent element. This means your icons respond to dark mode, hover states, and theme changes without any extra code.
/* Icons automatically match text color */
.nav-link {
color: #94a3b8;
}
.nav-link:hover {
color: #f8fafc;
}
.nav-link svg {
/* No color declaration needed — inherits from parent */
}Most quality icon libraries use currentColor by default, making this work automatically.
Using Icons in Vue.js
Vue icon usage follows a similar pattern:
<template>
<div>
<Search :size="24" color="currentColor" />
<Home :size="24" />
</div>
</template>
<script setup>
import { Search, Home } from 'lucide-vue-next'
</script>Using Icons with Tailwind CSS
If you use Tailwind CSS, Heroicons is the natural choice since it is built by the same team. But any SVG icon works with Tailwind's utility classes:
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
function SearchButton() {
return (
<button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700">
<MagnifyingGlassIcon className="w-5 h-5" />
Search
</button>
)
}Tailwind's w-* and h-* utilities control icon size, text-* utilities control color (via currentColor), and all transition and animation utilities work on SVG icons.
Where to Use Icons: Common UI Patterns
// Navigation Bars
The most common icon placement. Desktop navigation typically uses icon plus text label. Mobile navigation (tab bars) uses icon with a small text label beneath.
// Mobile tab bar pattern
function TabBar() {
return (
<nav style={{ display: 'flex', justifyContent: 'space-around', padding: '8px 0', borderTop: '1px solid #27272a' }}>
{[
{ icon: <Home size={24} />, label: 'Home' },
{ icon: <Search size={24} />, label: 'Search' },
{ icon: <Bell size={24} />, label: 'Alerts' },
{ icon: <User size={24} />, label: 'Profile' },
].map(item => (
<button key={item.label} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px', background: 'none', border: 'none', color: '#94a3b8', fontSize: '10px' }}>
{item.icon}
{item.label}
</button>
))}
</nav>
)
}// Buttons with Icons
Icons in buttons reinforce the action. Place the icon before the text for left-to-right languages.
<button>
<Download size={16} />
Download Report
</button>
<button>
<Trash2 size={16} />
Delete
</button>
<button>
<Plus size={16} />
Add New
</button>// Icon-Only Buttons
When space is constrained, use icon-only buttons with aria-label for accessibility:
<button aria-label="Close dialog">
<X size={20} />
</button>
<button aria-label="Toggle dark mode">
<Moon size={20} />
</button>// Form Inputs with Icons
Icons inside input fields provide visual context about expected input:
<div style={{ position: 'relative' }}>
<Search size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: '#94a3b8' }} />
<input
type="text"
placeholder="Search..."
style={{ paddingLeft: '40px' }}
/>
</div>// Status Indicators
Icons communicate state changes clearly:
function StatusBadge({ status }) {
const config = {
success: { icon: <CheckCircle size={16} />, color: '#34d399', label: 'Success' },
error: { icon: <XCircle size={16} />, color: '#f87171', label: 'Error' },
warning: { icon: <AlertTriangle size={16} />, color: '#fbbf24', label: 'Warning' },
info: { icon: <Info size={16} />, color: '#60a5fa', label: 'Info' },
}
const { icon, color, label } = config[status]
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', color }}>
{icon} {label}
</span>
)
}// Empty States
Large icons work well in empty state illustrations — the inbox is empty, no search results, no data yet:
function EmptyState() {
return (
<div style={{ textAlign: 'center', padding: '80px 20px', color: '#94a3b8' }}>
<Inbox size={64} strokeWidth={1} />
<h3>No messages yet</h3>
<p>When you receive messages, they will appear here.</p>
</div>
)
}// Sidebar Menus
Icons in sidebars help users identify sections before reading labels:
const menuItems = [
{ icon: <LayoutDashboard size={20} />, label: 'Dashboard', href: '/dashboard' },
{ icon: <Users size={20} />, label: 'Team', href: '/team' },
{ icon: <FileText size={20} />, label: 'Documents', href: '/docs' },
{ icon: <BarChart3 size={20} />, label: 'Analytics', href: '/analytics' },
{ icon: <Settings size={20} />, label: 'Settings', href: '/settings' },
]// Toast Notifications
Icons at the start of toast messages instantly convey severity:
function Toast({ type, message }) {
const icons = {
success: <CheckCircle size={20} color="#34d399" />,
error: <AlertCircle size={20} color="#f87171" />,
info: <Info size={20} color="#60a5fa" />,
}
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px 16px', background: '#18181b', border: '1px solid #27272a', borderRadius: '8px' }}>
{icons[type]}
<span>{message}</span>
</div>
)
}Icon Accessibility: The Complete Checklist
Accessibility is not optional. Inaccessible icons exclude users who rely on screen readers, keyboard navigation, or other assistive technologies.
// Decorative Icons (Hide from Screen Readers)
If an icon is next to a text label that already describes the action, the icon is decorative. Hide it from screen readers:
// The text "Settings" already describes the action
<a href="/settings">
<Settings size={20} aria-hidden="true" />
Settings
</a>// Meaningful Icons (Announce to Screen Readers)
If an icon is the only element conveying meaning (no visible text label), it needs an accessible label:
// Icon-only button — needs aria-label
<button aria-label="Delete item">
<Trash2 size={20} aria-hidden="true" />
</button>
// Or use a visually hidden label
<button>
<Trash2 size={20} aria-hidden="true" />
<span className="sr-only">Delete item</span>
</button>// Focus Indicators
Icon buttons must have visible focus indicators for keyboard users. Never remove the default outline without providing an alternative:
button:focus-visible {
outline: 2px solid #818cf8;
outline-offset: 2px;
}// Color Independence
Never use icon color alone to convey meaning. A red X and a green checkmark look identical to colorblind users. Always pair color with a distinct shape — a checkmark shape plus green, an X shape plus red.
// Touch Target Size
On mobile, every tappable icon must have a touch target of at least 44x44 CSS pixels, regardless of the visible icon size. Use padding to enlarge the tappable area:
.icon-button {
padding: 10px; /* 20px icon + 10px padding on each side = 40px minimum */
min-width: 44px;
min-height: 44px;
}Performance Optimization
Icons can impact page load speed and bundle size if handled carelessly.
// Tree Shaking
Always use named imports rather than importing the entire library:
// GOOD — only imports the Search icon (~200 bytes)
import { Search } from 'lucide-react'
// BAD — may import the entire library (~200KB+)
import * as Icons from 'lucide-react'// Lazy Loading Below-the-Fold Icons
Icons that are not visible on initial page load can be lazy loaded:
import dynamic from 'next/dynamic'
const LazyChart = dynamic(() => import('lucide-react').then(m => ({ default: m.BarChart3 })), {
loading: () => <div style={{ width: 20, height: 20 }} />,
})// SVG Sprite Generation
For server-rendered applications, generating an SVG sprite sheet avoids the overhead of individual component imports:
npx svg-sprite-generate --input ./icons --output ./public/sprite.svg
Then reference icons with minimal markup:
<svg width="24" height="24"> <use href="/sprite.svg#search" /> </svg>
// Avoid Icon Fonts
Icon fonts (like Font Awesome loaded as a web font) download the entire icon set regardless of how many icons you use. A typical icon font is 100KB to 300KB. The equivalent SVGs for the icons you actually use might total 5KB. For new projects, always use SVG icons.
Common Mistakes to Avoid
// Mixing Icon Libraries
Combining Lucide icons in the header with Heroicons in the sidebar and Font Awesome in the footer creates visual inconsistency. Different libraries have different stroke widths, corner radii, grid alignments, and design philosophies. Pick one library and use it consistently.
// Using Wrong Sizes
An icon at 14px is too small to be recognizable. An icon at 48px in a compact toolbar is disproportionate. Match icon size to context — 16px for dense UIs, 20-24px for standard interfaces, 32px+ for featured elements.
// Forgetting Dark Mode
Icons styled with hardcoded colors like color="#000000" will be invisible on dark backgrounds. Always use currentColor or CSS custom properties so icons adapt to theme changes automatically.
// Ignoring Loading States
If icons load from external URLs or large bundles, users may see layout shifts or empty spaces. Use skeleton placeholders or fixed-dimension containers to prevent layout instability.
// Overusing Icons
Not every list item, every heading, and every paragraph needs an icon. Icons should draw attention to important actions and navigation — when everything has an icon, nothing stands out. Use icons intentionally, not habitually.