Skip to content

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.

Authorhopes76 minutestailwindcss, icons, design-system, nextjs, lucide-react

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:

  1. Visual Consistency: All icons using the same design language
  2. Theme Adaptation: Automatic adaptation to dark/light themes
  3. Development Efficiency: Minimize cost of adding new icons
  4. 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:

  1. Consistent Design Style: All icons use the same line weight and rounded corners
  2. Good Theme Adaptation: Native support for currentColor
  3. TypeScript Support: Complete type definitions
  4. Excellent Performance: On-demand imports, tree-shaking friendly
  5. 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:

  1. Incremental Migration: Didn't try to replace all icons at once
  2. Tools First: Established good tools and conventions first
  3. Team Alignment: Ensured everyone understood the new usage approach
  4. 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:

  1. Automated Icon Validation: Check icon usage consistency in CI/CD
  2. Component Library Documentation: Establish complete component usage documentation
  3. Design Token Expansion: Extend successful experience to other design tokens like colors, spacing
  4. 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

#tailwindcss#icons#design-system#nextjs#lucide-react

Related Posts

November 26, 20256 minutes

From Coding to Verifying: Three Years of AI Collaboration as a Full-Stack Engineer

An original reflection on how AI reshapes full-stack development, shifting engineers from writing code to designing context, validating logic, and orchestrating human–AI workflows — with real backend and frontend examples.

#ai-assisted-development#frontend-ai#gemini3pro#engineering-productivity#code-quality