Rendering icons via CSS classes gains developer experience — but loses a central design property. The consequences for visual consistency across the entire product are significant.

The Problem in One Sentence

Stroke-based icon sets like Lucide, Phosphor, or Tabler offer a degree of freedom that filled icon sets like Google Material Symbols or Heroicons Solid inherently lack: control over line weight via stroke-width. This property determines whether an icon appears delicate (1 px), balanced (1.5 px), or bold (2 px), making it one of the most effective levers for fine-tuning the relationship between icons and typography. Rendering icons as CSS classes — via UnoCSS preset-icons, Tailwind plugins, or similar solutions — eliminates exactly this lever. The reason is technically simple, but the impact on a design system is substantial.

How CSS-Based Icon Rendering Works

Tools like UnoCSS preset-icons or the Iconify ecosystem convert SVG icons into CSS mask-image definitions. A multi-line SVG becomes a single CSS class:

/* What preset-icons makes from a Lucide icon */
.i-lucide-arrow-right {
  --un-icon: url("data:image/svg+xml,...");
  mask-image: var(--un-icon);
  -webkit-mask-image: var(--un-icon);
  mask-repeat: no-repeat;
  mask-size: 100% 100%;
  background-color: currentColor;
  width: 1em;
  height: 1em;
  display: inline-block;
}

The SVG becomes a mask. The visible area is determined by the icon’s silhouette, colored by background-color: currentColor. Elegant, performant, and done with a single utility class name in the markup.

But: a mask only knows area or no area. There is no “half transparent,” no “thinner stroke,” no “2 px instead of 1.5 px.” The stroke was resolved into a pixel area during rendering — the SVG property stroke-width no longer exists in the CSS context.

Why Stroke Weight Is Not a Detail Problem

Stroke weight is not a cosmetic detail. In a consistent design system, it serves three functions:

Optical weighting. Icons sit next to text. An icon with stroke-width: 2 next to a light font (300) appears disproportionately heavy. An icon with stroke-width: 1.25 next to a medium font (500) disappears. The stroke weight must match the typographic context — and that varies between headline, body, and caption.

Size adaptation. A 16 px icon optically needs a different stroke weight than the same icon at 24 px or 32 px. At small sizes, the stroke needs to be relatively thicker for the icon to remain legible. At large sizes, the same stroke appears clumsy. Lucide addresses this with a default stroke-width of 2 at a 24 px base size — but as soon as the size deviates, the ratio breaks.

Brand character. Stripe uses fine 1.5 px strokes. Notion uses bold 2 px. Linear works with 1.75 px. Stroke weight is part of visual identity — a parameter that runs through every page, every feature, every interface.

If you cannot control stroke weight, you accept the icon set’s default as an immutable part of your brand.

Comparing the Three Rendering Methods

1. CSS mask-image (preset-icons, Iconify CSS)

<span class="i-lucide-settings text-xl text-zinc-600"></span>
Aspect
Assessment
Developer experience
Excellent — one class name, done
Bundle size
Optimal — only used icons, as CSS
stroke-width controllable
No
Color controllable
Yes, via `color` / `currentColor`
Multi-color possible
No — monochrome only
Accessibility
`aria-hidden` must be added manually

Suited for: Projects that can live with the icon set’s default stroke and prioritize DX.

2. Inline SVG (Svelte component)

<script>
  import Settings from 'lucide-svelte/icons/settings';
</script>

<Settings size={20} strokeWidth={1.5} class="text-zinc-600" />
Aspect
Assessment
Developer experience
Good — import + props
Bundle size
Larger — SVG markup in HTML
stroke-width controllable
Yes, as a prop
Color controllable
Yes, via `color` prop or CSS
Multi-color possible
Yes, via CSS variables
Accessibility
Built-in (`aria-label`, `role`)

Suited for: Design systems that need full control over visual rendering.

3. SVG Sprite with <use>

<svg width="20" height="20" stroke-width="1.5">
  <use href="/icons.svg#settings" />
</svg>
Aspect
Assessment
Developer experience
Moderate — sprite must be generated and maintained
Bundle size
Good — one HTTP request for all icons, cacheable
stroke-width controllable
Partially — only if not hardcoded in the sprite
Color controllable
Yes, via CSS
Multi-color possible
Limited
Accessibility
Manual

Suited for: Large projects with many icon instances where HTTP requests need to be minimized.

What This Means for Design System Architecture

The question “How do we render icons?” is an infrastructure decision. It is made early, expensive to revise, and affects every component that contains an icon — which is nearly all of them.

For a design system that needs to guarantee consistency across different contexts, there is a clear decision tree:

Question 1: Does stroke weight vary by context? If icons in navigation (small, bold), hero sections (large, fine), and buttons (medium) should be weighted differently — inline SVG is the only option that enables this without workarounds.

Question 2: Is a uniform stroke across all sizes acceptable? If yes, CSS mask-image works excellently. Many icon sets are drawn so their default stroke works well at 20–24 px. If you stay within this range, you’ll barely notice the limitation.

Question 3: How many icons does the project use? For 20–40 icons, the bundle difference between CSS classes and inline SVG is negligible. For 200+ icons in an application, the DX of preset-icons becomes a real productivity gain.

The Hybrid Approach: CSS for Standard, SVG for Control

In practice, the answer is rarely binary. A pragmatic approach for SvelteKit projects:

<!-- Standard context: CSS icon suffices -->
<span class="i-lucide-chevron-right" aria-hidden="true"></span>

<!-- Context requiring specific weighting: inline SVG -->
<script>
  import ArrowRight from 'lucide-svelte/icons/arrow-right';
</script>
<ArrowRight size={32} strokeWidth={1.25} />

The prerequisite: the design system explicitly documents which rendering method applies in which context. Otherwise, an inconsistent mix develops over months whose inconsistency only becomes apparent when someone places all icons side by side.

Design tokens for icon weighting can capture the intention, even when implementation varies by rendering method:

:root {
  /* Icon stroke weight intention (for inline SVG) */
  --icon-stroke-sm: 1.75;   /* Navigation, compact UI */
  --icon-stroke-md: 1.5;    /* Standard, buttons, lists */
  --icon-stroke-lg: 1.25;   /* Hero sections, large icons */
}

These tokens only apply to inline SVG. But they document the design decision — and that is the first step toward a system that also enforces it.

Conclusion

The question “Can we control stroke weight?” sounds like a five-minute problem. The answer touches the architecture of the entire icon system. CSS-based rendering is not wrong — it is a deliberate trade-off: maximum DX against limited design control.

It becomes problematic when the trade-off is made unconsciously. Anyone who introduces preset-icons because the integration is elegant, only to discover six months later that icons look too coarse at 32 px and too thin at 14 px, has a migration problem.

The recommendation: document the rendering method as a deliberate design decision in the design system — with rationale, context rules, and the knowledge that mask-image knows nothing about SVG properties.