Building a Consistent Design System: Icon Standardization and Tailwind CSS Migration Journey
How we standardized our icon system and migrated to Tailwind CSS 4 in our Next.js 15 blog platform to achieve a unified design language.
Project Origin | Why We Had to Do This
To be honest, this problem had been bothering us for a long time.
Pain Points: A Chaotic Icon Ecosystem
Our hopes.icu project had accumulated various icon sources during iteration:
- Direct copy-pasted SVG code snippets
- Scattered inline
<svg>tags - Third-party icon libraries introduced at different times
- PNG format placeholder images and favicons
- Hand-drawn temporary icons
Even worse, these icons had completely inconsistent styles, sizes, and colors. Some used currentColor, others hardcoded #333, and some used outdated gradient effects. Sizes were all over the place: 16px, 18px, 20px, 24px mixed together, causing frequent alignment issues.
The Last Straw
The turning point came during last week's design review. The designer pointed out that our navigation bar icons displayed inconsistently in different themes: barely visible in dark mode, too prominent in light mode. Even more embarrassing was when we wanted to uniformly adjust all icon theme colors, we discovered we needed to manually modify files in over a dozen places—what a nightmare.
That evening, I spent three hours counting icon usage in the project:
# Statistics at the time
find . -name "*.tsx" -o -name "*.ts" -o -name "*.mdx" | xargs grep -l "svg\|icon" | wc -l
# Result: 27 files involved in icon usage
This number made me realize the problem's severity had exceeded simple fixes.
Goals: Establish a Unified Design Language
Our goals were clear:
- Visual Consistency: All icons using the same design language
- Theme Adaptation: Automatic adaptation to dark/light themes
- Development Efficiency: Minimize cost of adding new icons
- Maintainability: Unified management approach and API
Implementation Process | From Chaos to Order
Asset Inventory: Taking Stock
The first step was a comprehensive inventory of existing icon assets:
# Count various icon types
grep -r "lucide-react" . --include="*.tsx" | wc -l # 15
grep -r "<svg" . --include="*.tsx" | wc -l # 12
grep -r "className.*icon" . --include="*.tsx" | wc -l # 8
Results showed that while the project already partially used lucide-react, there were still many custom SVGs and inline icons.
Decision: Choosing lucide-react as Standard
After team discussion, we decided to fully adopt lucide-react as our icon standard, with solid reasons:
- Consistent Design Style: All icons use the same line weight and rounded corners
- Good Theme Adaptation: Native support for
currentColor - TypeScript Support: Complete type definitions
- Excellent Performance: On-demand imports, tree-shaking friendly
- Rich Icon Library: Covers all common icons we need
Unified Icon Component API
To simplify usage, we created a unified Icon component:
// components/ui/icon.tsx
import * as React from "react"
import type { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
type IconProps = {
name: LucideIcon
className?: string
title?: string
"aria-hidden"?: boolean
}
export function Icon({ name: IconComp, className, title, ...rest }: IconProps) {
return (
<IconComp
className={cn("w-5 h-5 inline-block align-middle", className)}
aria-hidden={title ? undefined : true}
aria-label={title}
{...rest}
/>
)
}
Migration Strategy: Phased Replacement
We developed a three-phase migration strategy:
Phase One: Core Components
- Header navigation icons
- Footer social icons
- Theme toggle button
Phase Two: Functional Pages
- Blog list icons
- Tag and category icons
- Search and filter icons
Phase Three: Detail Optimization
- Form control icons
- Status indicator icons
- Decorative icons
Each phase went through complete testing to ensure no existing functionality was broken.
Technical Details | Tailwind CSS 4 & Next.js 15 Implementation Experience
Tailwind CSS 4 Configuration Optimization
Tailwind CSS 4 brings exciting new features, especially better support for design tokens:
/* app/globals.css */
@theme {
/* Semantic icon sizes */
--size-icon-xs: 0.75rem;
--size-icon-sm: 1rem;
--size-icon-md: 1.25rem;
--size-icon-lg: 1.5rem;
}
/* Semantic color tokens */
:root {
--icon-primary: hsl(var(--primary));
--icon-muted: hsl(var(--muted-foreground));
--icon-accent: hsl(var(--accent));
}
Unified Naming Conventions
We established strict naming conventions:
// ✅ Correct usage
<Icon name={Home} className="size-5 text-foreground/90" />
<Icon name={Search} className="size-4 text-muted-foreground" />
// ❌ Avoid these
<Icon name={Home} className="w-5 h-4 text-blue-500" /> <!-- Inconsistent size -->
<Icon name={Search} className="size-5" /> <!-- Missing color semantics -->
Build Performance Optimization
lucide-react's on-demand import strategy benefited us greatly:
// ✅ Recommended: On-demand imports
import { Home, Search, Menu } from "lucide-react"
// ❌ Avoid: Bulk imports
import * as Icons from "lucide-react"
Our build configuration ensured only actually used icons were bundled:
// next.config.ts
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react']
}
}
Accessibility Practices
We paid special attention to icon accessibility:
// Decorative icons
<Icon name={Sparkles} className="size-4 text-primary" aria-hidden />
// Semantic icons
<Icon
name={ExternalLink}
className="size-4 text-muted-foreground"
title="External link"
/>
Comparison & Effects | Before and After Changes
Code Complexity Comparison
Before Migration:
<!-- Messy icon code -->
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
After Migration:
<!-- Clean icon code -->
<Icon name={Home} className="size-6 text-foreground" />
Theme Adaptation Comparison
Before Migration:
/* Need to handle each icon separately */
.icon-home { color: var(--color-text); }
.icon-search { color: #666; } /* Hardcoded! */
.icon-menu { fill: currentColor; }
After Migration:
/* Unified theme adaptation */
.icon { color: hsl(var(--foreground)); }
.icon-muted { color: hsl(var(--muted-foreground)); }
Development Experience Improvement
The most obvious change was improved development efficiency:
| Scenario | Before Migration | After Migration |
|---|---|---|
| Add new icon | Copy SVG → Adjust styles → Test | import { IconName } → Use directly |
| Theme adaptation | Manually modify multiple CSS files | Auto-inherit theme colors |
| Size adjustment | Modify width/height individually | Unified size-* classes |
| Type safety | No type checking | Complete TypeScript support |
Experience Summary & Next Steps
Success Factors
Looking back at the entire migration process, these factors were crucial:
- Incremental Migration: Didn't try to replace all icons at once
- Tools First: Established good tools and conventions first
- Team Alignment: Ensured everyone understood the new usage approach
- Continuous Validation: Each phase went through complete testing
Pitfalls Review
Honestly, we encountered quite a few problems during the process:
Size Alignment Issues
Initial w-5 h-5 caused alignment issues in some components, so we later switched to semantic size-* classes:
/* tailwind.config.ts */
theme: {
extend: {
spacing: {
'4.5': '1.125rem', /* 18px */
}
}
}
Legacy Style Residue Some old inline styles were hard to discover, so we added ESLint rules to detect them:
// eslint.config.js
rules: {
'react/no-danger-with-children': 'error',
'@typescript-eslint/no-unused-vars': 'error',
}
Future Plans
Based on this successful experience, we plan to:
- Automated Icon Validation: Check icon usage consistency in CI/CD
- Component Library Documentation: Establish complete component usage documentation
- Design Token Expansion: Extend successful experience to other design tokens like colors, spacing
- Dynamic Icon Loading: On-demand loading of large icon sets
Conclusion
This icon standardization migration brought us more than just cleaner code; more importantly, it established a maintainable design system. In the process from chaos to order, we learned how to balance development efficiency and design consistency.
Standardization doesn't limit creativity—it prevents creativity from being consumed by disorder. When our basic components are reliable enough, the team can focus on true innovation—creating better experiences for users.
In the subsequent development of hopes.icu, this design system will continue to play a role, allowing us to iterate quickly without sacrificing quality. This is the value of technical infrastructure: it silently supports product growth, making innovation more从容.
Related Resources:
Tech Tags: #design-system #icon-standardization #TailwindCSS #NextJS15 #frontend-engineering