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:
- Audit your current icon usage
- Design semantic classes for your common patterns
- Implement in your CSS framework’s component system
- Migrate systematically, file by file
- 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.