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…
⚛️ Advanced React Development Guides
Comprehensive React guides covering hooks, performance optimization, and React 19 features. Includes code examples and prompts to boost your workflow.
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:
| Component | Use Case | Example |
|---|---|---|
Button | All clickable actions | CTAs, form submission |
Card | Container for content | Feature cards, testimonials |
Dialog | Modal overlays | Forms, confirmations |
Input | Text input fields | Search, forms |
Select | Dropdown selection | Filters, options |
Carousel | Image carousels | Project showcase |
Accordion | Expandable sections | FAQs, features |
Tabs | Tab panels | Different views |
Badge | Labels/tags | Status, 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.
Creating Collections: Reusable Data Entities — Types & Examples
Step-by-step guide to define collection types, create example data, export for blocks, and prepare for Payload…
Block Component Templates: Quick Reference & Checklists
Copy-paste TypeScript + React templates, example data, and practical checklists for building blocks, collections, and…