//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Create reusable, accessible SVG icon components in React. Use inline SVG for full control, SVGR for importing .svg files as components, or an SVG sprite for optimal performance with many icons.
Inline SVG as a component:
// components/icons/check-icon.tsx
interface IconProps {
size?: number;
color?: string;
className?: string;
"aria-label"?: string;
}
export function CheckIcon({ size = 24, color = "currentColor", className, ...props }: IconProps) {
const isDecorative = !props["aria-label"];
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
role={isDecorative ? undefined : "img"}
aria-hidden={isDecorative ? true : undefined}
aria-label={props["aria-label"]}
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}A custom icon set component library with consistent API and accessibility:
// components/icons/icon.tsx
import { type SVGProps } from "react";
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "children"> {
size?: number;
label?: string;
}
function createIcon(path: React.ReactNode, displayName: string) {
function Icon({ size = 24, label, className, ...props }: IconProps) {
const isDecorative = !label;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
role={isDecorative ? undefined : "img"}
aria-hidden={isDecorative ? true : undefined}
aria-label={label}
{...props}
>
{path}
</svg>
);
}
Icon.displayName = displayName;
return Icon;
}
// Define icons
export const ArrowRight = createIcon(
<line x1="5" y1="12" x2="19" y2="12"><polyline points="12 5 19 12 12 19" /></line>,
"ArrowRight"
);
export const Close = createIcon(
<><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></>,
"Close"
);
export const Heart = createIcon(
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />,
"Heart"
);
export const Star = createIcon(
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />,
"Star"
);
export const Search = createIcon(
<><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></>,
"Search"
);
export const Menu = createIcon(
<><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="18" x2="21" y2="18" /></>,
"Menu"
);// app/components/icon-showcase.tsx
"use client";
import { useState } from "react";
import { ArrowRight, Close, Heart, Star, Search, Menu } from "@/components/icons/icon";
import type { IconProps } from "@/components/icons/icon";
const icons = [
{ component: ArrowRight, name: "ArrowRight" },
{ component: Close, name: "Close" },
{ component: Heart, name: "Heart" },
{ component: Star, name: "Star" },
{ component: Search, name: "Search" },
{ component: Menu, name: "Menu" },
];
export function IconShowcase() {
const [iconSize, setIconSize] = useState(24);
const [iconColor, setIconColor] = useState("#374151");
return (
<div className="space-y-6">
<div className="flex items-center gap-6">
<label className="flex items-center gap-2 text-sm">
Size:
<input
type="range"
min={16}
max={48}
value={iconSize}
onChange={(e) => setIconSize(Number(e.target.value))}
className="w-32"
/>
<span className="w-8 text-right">{iconSize}</span>
</label>
<label className="flex items-center gap-2 text-sm">
Color:
<input
type="color"
value={iconColor}
onChange={(e) => setIconColor(e.target.value)}
className="h-8 w-8 cursor-pointer"
/>
</label>
</div>
<div className="grid grid-cols-3 gap-4 sm:grid-cols-6">
{icons.map(({ component: Icon, name }) => (
<div
key={name}
className="flex flex-col items-center gap-2 rounded-lg border border-gray-200 p-4"
>
<Icon size={iconSize} color={iconColor} label={name} />
<span className="text-xs text-gray-500">{name}</span>
</div>
))}
</div>
</div>
);
}currentColor value makes SVG icons inherit the parent element's color CSS property, enabling styling with Tailwind text color classes.viewBox="0 0 24 24" attribute defines the coordinate space. The width and height attributes control the rendered size independently.createIcon factory function keeps the API consistent across all icons while avoiding repeated boilerplate.aria-hidden="true") or informative (role="img" with aria-label).SVGR for importing .svg files as React components:
npm install @svgr/webpack// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: [
{
loader: "@svgr/webpack",
options: {
svgoConfig: {
plugins: [{ name: "removeViewBox", active: false }],
},
},
},
],
});
return config;
},
};
export default nextConfig;// Now import SVGs as components
import Logo from "@/public/icons/logo.svg";
export function Header() {
return <Logo className="h-8 w-8 text-blue-600" />;
}SVG sprite approach for many icons:
// public/icons/sprite.svg
// <svg xmlns="http://www.w3.org/2000/svg">
// <symbol id="icon-check" viewBox="0 0 24 24">
// <polyline points="20 6 9 17 4 12" />
// </symbol>
// <symbol id="icon-close" viewBox="0 0 24 24">
// <line x1="18" y1="6" x2="6" y2="18" />
// <line x1="6" y1="6" x2="18" y2="18" />
// </symbol>
// </svg>
interface SpriteIconProps {
name: string;
size?: number;
className?: string;
label?: string;
}
export function SpriteIcon({ name, size = 24, className, label }: SpriteIconProps) {
return (
<svg
width={size}
height={size}
className={className}
role={label ? "img" : undefined}
aria-hidden={label ? undefined : true}
aria-label={label}
>
<use href={`/icons/sprite.svg#icon-${name}`} />
</svg>
);
}
// Usage: <SpriteIcon name="check" size={20} className="text-green-500" />SVGProps<SVGSVGElement> to allow all native SVG attributes on your icon components.Omit utility to remove children from the SVG props since icon content is fixed..svg imports.// types/svg.d.ts
declare module "*.svg" {
import type { FC, SVGProps } from "react";
const content: FC<SVGProps<SVGSVGElement>>;
export default content;
}strokeWidth, viewBox, fillRule) instead of kebab-case. React will warn about invalid DOM properties otherwise.viewBox attribute when optimizing SVGs with SVGO. Removing it breaks scaling behavior.fill and stroke properties default to black in SVG. Set fill="none" for stroke-based icons or stroke="none" for filled icons explicitly.title element by default, which creates a tooltip on hover. Disable it with titleProp: false in SVGR config if unwanted.use href do not support CSS currentColor for cross-origin sprites. The sprite must be on the same domain.| Approach | Pros | Cons |
|---|---|---|
| Inline SVG components | Full control, tree-shakeable, CSS styling | Verbose, increases bundle per icon |
| SVGR imports | Use .svg files directly, auto-optimization | Requires webpack config, build step |
| SVG sprites | Single request for all icons, cached | No tree-shaking, manual sprite management |
| Icon fonts | Tiny bundle, familiar CSS usage | Blurry at small sizes, limited styling |
| Icon libraries (Lucide) | Ready-made, consistent, well-maintained | External dependency, less control |
.svg files as React components via a webpack loader.<use href>.stroke="currentColor" makes the icon inherit the parent's CSS color property.className="text-blue-500" on the parent or icon, and the stroke color updates automatically.size, label, and className props.displayName for React DevTools.aria-hidden="true" and omit role.role="img" and provide aria-label with a description.label prop is provided to decide.viewBox breaks scaling behavior entirely.width/height changes.{ name: "removeViewBox", active: false }.// next.config.ts
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: [{ loader: "@svgr/webpack" }],
});
return config;
}Then: import Logo from "@/public/icons/logo.svg";
// types/svg.d.ts
declare module "*.svg" {
import type { FC, SVGProps } from "react";
const content: FC<SVGProps<SVGSVGElement>>;
export default content;
}<symbol> elements in a single SVG file.<use href="/icons/sprite.svg#icon-name">.currentColor does not work for cross-origin sprites; the sprite must be on the same domain.black.fill="none".stroke="none".strokeWidth not stroke-width, viewBox not viewbox.export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "children"> {
size?: number;
label?: string;
}Use Omit to remove children since icon content is fixed.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥