Inline Components vs. External Images
An icon system for a modern web app involves tradeoffs between interactivity, accessibility, and bundle size.
The first decision: should icons be loaded as external <img src="/icon.svg" /> files or as inline React components?
External <img> references cache well at the network level, but they create a strict boundary. You cannot change the fill color of an external SVG with CSS on hover, and you cannot transition its stroke for dark mode. Dynamic theming requires inline React components.
The catch: compiling hundreds of inline SVGs into your bundle can hurt Time to Interactive (TTI) if you don't structure the exports correctly.
Structuring Exports for Tree-Shaking
How you export icon components affects both developer experience and bundle size.
Default exports (export default MyIcon) let consumers import a module under any name, which breaks automated refactoring and creates naming drift. Worse, importing from a monolithic barrel file (index.ts re-exporting everything) can force the bundler to parse the entire icon library even when you need a single icon.
Use named exports with direct file imports instead:
// ❌ Barrel import — bundler may pull in the entire library
import { HomeIcon, ProfileIcon } from '@/components/icons';
// ✅ Direct file imports — only the used icons enter the bundle
import { HomeIcon } from '@/components/icons/HomeIcon';
import { ProfileIcon } from '@/components/icons/ProfileIcon';
Named exports enforce consistent naming, enable IDE auto-imports, and let bundlers statically analyze which components are unused.
Dynamic Theming with currentColor
Setting SVG paths to fill="currentColor" or stroke="currentColor" makes the vector inherit the computed text color of its parent element. No prop drilling needed.
export const SettingsIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
</svg>
);
In Tailwind, <SettingsIcon className="text-slate-800 dark:text-slate-100" /> handles dark mode with zero JavaScript overhead.
Dimension Strategies: Fluid vs. Fixed
How you set the root SVG dimensions affects layout stability and responsiveness:
- Fixed (
width={24} height={24}): Prevents layout shift (CLS) during render, but requires CSS overrides for responsive scaling. - Fluid (no width/height, keep
viewBox): The browser calculates size from the parent container. Works well when parent sizing is well-defined. - Em-based (
width="1em" height="1em"): Scales relative to the parent'sfont-size, so the icon behaves like text. Aligns cleanly inside buttons and inline content.
SVGO: Optimization Before Conversion
Raw SVGs from design tools carry bloated metadata. Run SVGO before converting to JSX, but watch the defaults:
- Disable
removeViewBox— without it, fluid scaling breaks. - Handle
cleanupIDscarefully — aggressive cleanup can sever internal gradient references. - Enable
prefixIds— prevents ID collisions when multiple SVGs share the DOM.
A good SVG to JSX Converter applies these rules during conversion, so you get optimized, collision-safe React components without managing SVGO config yourself.