Building a Semantic Icon Sizing System


Icon sizing in web projects often starts simple but quickly spirals into chaos. What begins as a few size-4 classes scattered across components evolves into an inconsistent mess of h-4 w-4, size-3, h-2.5 w-2.5, and manual pixel values that nobody wants to touch.

After auditing our codebase, I found 6 different icon sizes used across 17+ files with no clear system or meaning behind the choices. A simple change like “make all navigation icons slightly larger” would require hunting through dozens of components. Time for a better approach.

The Problem: Icon Sizing Chaos

Here’s what we were dealing with:

<!--Navigation icons-->
<Icon class="h-4 w-4 drop-shadow-[0px_1.5px_1.5px_rgba(0,0,0,0.175)]" name="menu" />
 
<!--Close buttons-->
<Icon class="size-4 hover:scale-110 transition-transform" name="cancel" />
 
<!--RSS feeds-->
<Icon class="h-2.5 w-2.5 opacity-70" name="rss" />
 
<!--Theme toggle-->
<Icon class="size-4 transition-all dark:scale-0" name="sun" />

Problems:

  • No semantic meaning - size-4 tells you nothing about the icon’s purpose
  • Inconsistent styling - Drop shadows, transitions, and effects scattered everywhere
  • Maintenance nightmare - Want larger nav icons? Edit 8+ files manually
  • Cognitive overload - Developers constantly making sizing decisions
  • Copy-paste errors - Easy to miss styling when reusing icons

The Solution: Semantic Icon Classes

Instead of fighting the chaos, we built a systematic approach using Tailwind’s component system:

// tailwind.config.ts
addComponents({
  // Size-based classes
  ".icon-sm": {
  "@apply size-3 aspect-square": {}, // 12px - decorative
  },
  ".icon-base": {
  "@apply size-4 aspect-square": {}, // 16px - standard
  },
  ".icon-lg": {
  "@apply size-5 aspect-square": {}, // 20px - prominent
  },
  ".icon-xl": {
  "@apply size-6 aspect-square": {}, // 24px - large interactive
  },
 
  // Context-specific classes
  ".icon-nav": {
  "@apply size-4 aspect-square drop-shadow-[0px_1.5px_1.5px_rgba(0,0,0,0.175)]": {},
  },
  ".icon-close": {
  "@apply size-4 aspect-square hover:scale-110 transition-transform": {},
  },
  ".icon-rss": {
  "@apply size-3 aspect-square opacity-70 hover:opacity-100 transition-opacity": {},
  },
  ".icon-toggle": {
  "@apply size-4 aspect-square transition-all": {},
  },
  ".icon-action": {
  "@apply size-4 aspect-square hover:text-accent-two transition-colors": {},
  },
})

The Transform: Before vs After

Before: Manual, inconsistent, unmaintainable

<Icon class="h-4 w-4 drop-shadow-[0px_1.5px_1.5px_rgba(0,0,0,0.175)]" name="menu" />
<Icon class="size-4 hover:scale-110 transition-transform aspect-square" name="cancel" />
<Icon class="h-2.5 w-2.5 opacity-70 hover:opacity-100 aspect-square" name="rss" />

After: Semantic, consistent, maintainable

<Icon class="icon-nav" name="menu" />
<Icon class="icon-close" name="cancel" />
<Icon class="icon-rss" name="rss" />

Implementation Strategy

1. Audit First

We searched the entire codebase for icon usage patterns:

grep -r "h-\d+.*w-\d+\|w-\d+.*h-\d+\|size-\d+" src/

This revealed our 6 different sizes and their inconsistent usage across components.

2. Design the System

We created two types of classes:

  • Size-based (icon-sm, icon-base, icon-lg, icon-xl) - Pure sizing
  • Context-based (icon-nav, icon-close, icon-rss) - Size + behavior

3. Migrate Systematically

Rather than a big-bang rewrite, we migrated file-by-file:

  • Header navigation icons → icon-nav
  • Close buttons → icon-close
  • RSS feeds → icon-rss
  • Action buttons → icon-action
  • Theme toggles → icon-toggle

4. Validate Results

A final search confirmed no manual sizing remained:

grep -r "Icon.*size-\d+\|Icon.*h-\d+" src/
# No matches found ✅

The Benefits: Why This Approach Wins

Dramatic Code Reduction

<!--82 characters-->
<Icon class="h-4 w-4 drop-shadow-[0px_1.5px_1.5px_rgba(0,0,0,0.175)] aspect-square" />
 
<!--21 characters-->
<Icon class="icon-nav" />

Global Control

Want all navigation icons larger? One line in the config:

".icon-nav": {
  "@apply size-5": {}, // All nav icons now 20px everywhere
}

Self-Documenting Code

  • icon-nav tells you exactly what this icon does
  • New developers understand intent immediately
  • No guessing about appropriate sizing

Responsive by Design

Easy to add breakpoint-specific sizing:

".icon-nav": {
  "@apply size-3 md:size-4 lg:size-5": {}, // Grows with viewport
}

Consistency Enforced

  • Impossible to accidentally use wrong sizing
  • All icons of same type look identical
  • Designers’ intentions preserved

Lessons Learned

Start Semantic from Day One

Don’t wait for chaos to emerge. Build semantic systems early:

<!--Avoid this-->
<Icon class="size-4" />
 
<!--Do this-->
<Icon class="icon-nav" />

Context Beats Size

icon-close is more valuable than icon-lg because it encodes meaning, not just dimensions.

Tailwind Components Scale

Using addComponents() in Tailwind config gives you:

  • IntelliSense autocomplete
  • Consistent API
  • Easy global changes
  • No performance overhead

Migration Is Worth It

Yes, updating 17+ files takes time. But the maintenance savings and developer experience improvements pay back quickly.

Implementation Tips

Naming Convention

We used icon-{context} for specific use cases and icon-{size} for generic sizing:

  • icon-nav, icon-close, icon-rss (context-specific)
  • icon-sm, icon-base, icon-lg (generic sizes)

Override Patterns

Keep escape hatches for edge cases:

<!--Use semantic class first-->
<Icon class="icon-nav" />
 
<!--Override when truly needed-->
<Icon class="icon-nav!size-6" />

Team Communication

Document the system clearly:

  • Which class for which context?
  • When to use generic vs specific classes?
  • How to request new icon types?

How to Control Icon Sizing

With the semantic system in place, you have multiple levels of control from global to specific:

Global Control (Most Powerful)

Edit tailwind.config.ts to change all icons of a type across your entire codebase:

// Make all navigation icons larger
".icon-nav": {
  "@apply size-5": {}, // Changed from size-4 - affects every nav icon!
},
 
// Make RSS icons more prominent
".icon-rss": {
  "@apply size-4": {}, // Changed from size-3 - all RSS icons now larger
},

Responsive Control

Add breakpoint-specific sizing:

".icon-nav": {
  "@apply size-3 sm:size-4 lg:size-5": {},
  // Small on mobile, medium on tablet, large on desktop
},

Component-Level Control

Override for specific cases:

<!--Use semantic class first-->
<Icon class="icon-nav" name="menu" />
 
<!--Override when needed-->
<Icon class="icon-nav!size-6" name="menu" /> <!--Force larger-->

Current Size Reference

  • icon-sm - 12px (decorative, RSS)
  • icon-base - 16px (standard UI)
  • icon-lg - 20px (prominent actions)
  • icon-xl - 24px (large interactive)

Example: Making All Icons Touch-Friendly

// Three lines = entire website becomes more touch-friendly
".icon-nav": { "@apply size-5": {} }, // 16px → 20px
".icon-close": { "@apply size-5": {} }, // 16px → 20px
".icon-action": { "@apply size-5": {} }, // 16px → 20px

Result: Every navigation, close, and action icon across your entire website becomes larger. No hunting through 17+ files.

The Result: A System That Scales

What started as a refactoring exercise became a design system foundation. Our icon sizing is now:

  • Consistent across all components
  • Maintainable with global control
  • Semantic with self-documenting code
  • Scalable for future growth
  • Developer-friendly with clear conventions

The best part? This approach extends beyond icons. We’re applying the same semantic patterns to spacing, typography, and component variants.

Try It Yourself

If you’re fighting icon sizing chaos, try this approach:

  1. Audit your current icon usage
  2. Design semantic classes for your common patterns
  3. Implement in your CSS framework’s component system
  4. Migrate systematically, file by file
  5. Validate that no manual sizing remains

The upfront investment pays dividends in maintainability, consistency, and developer happiness.---Building design systems isn’t just about the big architectural decisions—sometimes it’s about solving the small, daily frustrations that slow teams down. Icon sizing might seem trivial, but good systems make the trivial things invisible.