3 Proven Practices: Lucide React Icons & shadcn/ui

Enforce UI consistency with Lucide React icons, shadcn/ui components, and a reusable CTA type to cut duplication and…

·Matija Žiberna·

⚛️ Advanced React Development Guides

Comprehensive React guides covering hooks, performance optimization, and React 19 features. Includes code examples and prompts to boost your workflow.

No spam. Unsubscribe anytime.

Part 5 of the Design to Code series — Following Creating Collections

Building consistent, maintainable code means leveraging existing tools effectively. This guide covers three critical practices: using Lucide React for icons, using shadcn/ui for components, and reusing the CTA type everywhere.

These practices prevent code duplication, ensure consistency, and make global changes trivial.

Three Practices for Consistency

Practice 1: Icons with Lucide React

Rule: Always use Lucide React. Never import SVGs directly.**

Lucide is already in your project. Every icon is consistent, customizable, and type-safe. Building a custom SVG system is wasted effort.

How to Use Lucide

In your types** (store icon as string name):

export interface Feature {
  icon?: string;  // Icon name as string: 'Zap', 'Heart', 'Star'
}

export interface Industry {
  icon?: string;  // Same pattern everywhere
}

In your components** (resolve string to component):

import { Zap, Heart, Star, ShoppingBag, LucideIcon } from 'lucide-react';

const iconMap: Record<string, LucideIcon> = {
  Zap,
  Heart,
  Star,
  ShoppingBag,
  // Add more as needed
};

export function MyComponent({ items }: Props) {
  return (
    <div>
      {items.map(item => {
        const Icon = iconMap[item.icon || 'Star'];  // Fallback to Star
        return (
          <div key={item.id}>
            {Icon && <Icon className="w-6 h-6 text-blue-600" />}
            <span>{item.title}</span>
          </div>
        );
      })}
    </div>
  );
}

In your data** (use icon name):

const featureItem = {
  id: 1,
  title: 'Fast Setup',
  icon: 'Zap',  // ← String name, not component
};

Why This Approach

  • Consistent icons everywhere: Same icon set, same styling
  • Data-driven: Icons come from data, not hardcoded in components
  • Easy to change: Update icon name in data, reflects everywhere
  • Type-safe: TypeScript knows which icons exist
  • Customizable: Size, color, stroke width via className

Common Lucide Icons

Popular icons you'll use:

  • Arrows: ArrowRight, ArrowLeft, ChevronDown, ChevronUp
  • Objects: Heart, Star, Zap, Mail, Phone, MapPin
  • UI: Menu, X, Search, Bell, Settings, User
  • Categories: ShoppingBag, Briefcase, Heart, Lightbulb, Target
  • Status: Check, CheckCircle, AlertCircle, Info, Download

Full list: lucide.dev

Practice 2: Components with shadcn/ui

Rule: Use shadcn/ui for all generic UI. Never create custom Button, Card, Dialog, etc.**

shadcn/ui components are already in your project. They're accessible, consistent, and customizable. Building custom components duplicates work.

How to Use shadcn/ui

Import what you need:

import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';

Use them as the base:

export function MyComponent() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>My Section</CardTitle>
      </CardHeader>
      <CardContent>
        <p>Content here</p>
        <Button variant="primary">Click Me</Button>
      </CardContent>
    </Card>
  );
}

Customize with Tailwind:

// If Figma shows a blue card with rounded corners and shadow:
<Card className="border-2 border-blue-600 rounded-lg shadow-lg p-6">
  <CardTitle className="text-xl text-blue-600">Title</CardTitle>
</Card>

// If you need custom spacing:
<Button
  className="px-8 py-4 text-lg font-bold rounded-xl"
  style={{
    backgroundColor: '#0066ff',
    borderRadius: '12px',
  }}
>
  Custom Button
</Button>

Never do this:

// ❌ DON'T: Create custom button
function MyCustomButton() {
  return <button className="...">Click</button>;
}

// ❌ DON'T: Create custom card
function FeatureCard() {
  return <div className="...">Content</div>;
}

// ✅ DO: Use shadcn/ui and customize
<Button className="custom-classes">Click</Button>
<Card className="custom-classes">Content</Card>

Available shadcn/ui Components

Commonly used in blocks:

ComponentUse CaseExample
ButtonAll clickable actionsCTAs, form submission
CardContainer for contentFeature cards, testimonials
DialogModal overlaysForms, confirmations
InputText input fieldsSearch, forms
SelectDropdown selectionFilters, options
CarouselImage carouselsProject showcase
AccordionExpandable sectionsFAQs, features
TabsTab panelsDifferent views
BadgeLabels/tagsStatus, categories

Customization Examples

Matching Figma Blue Gradient Button:**

Figma shows: Blue gradient background, white text, rounded 8px

<Button
  className="bg-gradient-to-r from-blue-500 to-blue-700 text-white hover:from-blue-600 hover:to-blue-800 rounded-lg"
>
  Explore Features
</Button>

Matching Figma Minimal Card:**

Figma shows: Light border, no shadow, padding 24px

<Card className="border border-gray-200 shadow-none p-6">
  <CardHeader className="pb-4">
    <CardTitle>Card Title</CardTitle>
  </CardHeader>
  <CardContent>Content</CardContent>
</Card>

Practice 3: Reuse CTA Type Everywhere

The CTA (Call-To-Action) type is the most reusable type in your project. Use it for every button, link, and actionable element.

What is CTA?

export interface CTA {
  label?: string;        // Button text
  href?: string;         // URL
  external?: boolean;    // Open in new tab
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
  icon?: string;         // Lucide icon name
  disabled?: boolean;
}

Where to Use CTA

In any block with buttons:

export interface FeaturesBlock {
  items?: Feature[];
  cta?: CTA;  // Primary block CTA
}

export interface Feature {
  title: string;
  description: string;
  cta?: CTA;  // CTA on each item
}

In collections:

export interface Industry {
  title: string;
  link?: CTA;  // Link to detail page
}

export interface TeamMember {
  name: string;
  socialLinks?: CTA[];  // Multiple links
}

In any component with interactive elements:

export interface HeroSection {
  title: string;
  primaryCta?: CTA;
  secondaryCta?: CTA;
}

Creating a CTA Button Component

Create a reusable component that handles CTA rendering:

File: src/components/cta-button.tsx

import type { CTA } from '@/types/blocks';
import { Button } from '@/components/ui/button';

interface CTAButtonProps {
  cta: CTA | undefined;
  className?: string;
}

export function CTAButton({ cta, className }: CTAButtonProps) {
  if (!cta) return null;

  return (
    <Button
      variant={cta.variant || 'primary'}
      disabled={cta.disabled}
      className={className}
      asChild
    >
      <a
        href={cta.href || '#'}
        target={cta.external ? '_blank' : '_self'}
        rel={cta.external ? 'noopener noreferrer' : undefined}
      >
        {cta.label || 'Learn More'}
      </a>
    </Button>
  );
}

Now use it everywhere:

// In feature cards:
<CTAButton cta={feature.cta} />

// In hero section:
<CTAButton cta={data.primaryCta} className="text-lg" />

// In industry cards:
<CTAButton cta={industry.link} variant="outline" />

One component handles all CTA rendering consistently.

Benefit: One Change Fixes Everything

Update the CTA type or component once. Affects every button in your app.

// Add tracking to CTA
export interface CTA {
  label?: string;
  href?: string;
  tracking?: {
    event: string;
    properties?: Record<string, string>;
  };
}

// CTAButton component handles tracking:
export function CTAButton({ cta }: CTAButtonProps) {
  const handleClick = () => {
    if (cta?.tracking) {
      analytics.track(cta.tracking.event, cta.tracking.properties);
    }
  };

  return (
    <Button onClick={handleClick}>
      <a href={cta?.href}>{cta?.label}</a>
    </Button>
  );
}

// Every button in the app now tracks! No changes needed to component files.

This is the power of reusable types.

Putting It All Together

Here's how these three practices work in a real component:

import { Heart, Star, Zap } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CTAButton } from '@/components/cta-button';
import type { Feature } from '@/types';

interface FeaturesGridProps {
  features: Feature[];
}

const iconMap = { Heart, Star, Zap };

export function FeaturesGrid({ features }: FeaturesGridProps) {
  return (
    <div className="grid grid-cols-3 gap-6">
      {features.map(feature => {
        const Icon = iconMap[feature.icon || 'Star'];
        return (
          <Card key={feature.id} className="hover:shadow-lg">
            <CardHeader>
              {Icon && <Icon className="w-8 h-8 text-blue-600 mb-4" />}
              <CardTitle>{feature.title}</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-gray-600 mb-4">{feature.description}</p>
              <CTAButton cta={feature.cta} variant="outline" />
            </CardContent>
          </Card>
        );
      })}
    </div>
  );
}

This component:

  • Uses Lucide icons (iconMap pattern)
  • Uses shadcn/ui (Card, CardHeader, CardTitle)
  • Reuses CTA type (CTAButton)
  • Customizes with Tailwind (className)
  • Data-driven (all content from props)
  • Consistent across the app

Consistency Rules

Follow these rules to maintain consistency across your entire application:

Icons:

  • Store as string names in data
  • Resolve in components using iconMap
  • Use Lucide React always
  • Never import custom SVGs
  • Never hardcode icon components

Components:

  • Use shadcn/ui for Button, Card, Dialog, Input, etc.
  • Customize with Tailwind className
  • Create custom components only for block-specific layouts
  • Never rebuild Button, Card, or standard UI elements
  • Never hardcode styles; use classes or type variants

CTAs:

  • Use CTA type for every button/link
  • Create CTAButton component once, use everywhere
  • Update CTA type once, fixes everything
  • Never create different button types for different sections
  • Never hardcode link behavior

These simple rules prevent duplication, ensure consistency, and make maintaining your codebase trivial.

0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.