Guide for proper shadcn-ui component usage - use Card for wrapping/layout, compose from base components, never modify components/ui directly
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: shadcn-ui-best-practices description: Guide for proper shadcn-ui component usage - use Card for wrapping/layout, compose from base components, never modify components/ui directly
shadcn-ui Best Practices
This guide covers best practices for working with shadcn-ui components in this Vite + React + TypeScript boilerplate.
Core Principles
1. Never Modify components/ui/ Directly
CRITICAL RULE: Components in src/components/ui/ are base primitives generated by shadcn-ui CLI. Never edit these files directly.
Why?
- These files can be regenerated or updated by the CLI
- Manual changes will be lost when updating components
- Breaking the abstraction makes maintenance difficult
What to do instead: Compose new components in src/components/shared/ that use these primitives.
Directory Structure
src/
βββ components/
β βββ ui/ # Base shadcn primitives (DO NOT MODIFY)
β β βββ button.tsx
β β βββ card.tsx
β β βββ badge.tsx
β β βββ ...
β βββ shared/ # Your composed app components (CREATE HERE)
β βββ FeatureCard.tsx
β βββ StatCard.tsx
β βββ ...
βββ pages/ # Page components (USE ui components here)
β βββ Home.tsx
βββ lib/
βββ utils.ts # cn() utility for className merging
Card Component Usage
When to Use Card
Cards are the go-to component for:
- Wrapping and layout: Creating visual containers for content
- Grouping related content: Features, stats, user info, etc.
- Creating sections: Distinct areas in your UI
- Interactive elements: Clickable panels, hover effects
Card Anatomy
shadcn-ui Card comes with semantic sub-components:
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>
{/* Main content */}
</CardContent>
<CardFooter>
{/* Actions or metadata */}
</CardFooter>
</Card>
Real Example from Home.tsx
// Feature cards with hover effects and gradient borders
<Card
key={feature.title}
className="group relative overflow-hidden border-2 transition-all duration-300 hover:-translate-y-2 hover:border-transparent hover:shadow-2xl"
>
{/* Gradient border effect on hover */}
<div
className={`absolute inset-0 -z-10 bg-gradient-to-br ${feature.gradient} opacity-0 transition-opacity duration-300 group-hover:opacity-100`}
/>
<div className="absolute inset-[2px] -z-10 rounded-lg bg-white" />
<CardHeader className="space-y-4">
<div className="flex items-start justify-between">
<div className={`rounded-xl bg-gradient-to-br ${feature.gradient} p-3`}>
<Icon className="h-6 w-6 text-white" />
</div>
<Badge>{feature.badge}</Badge>
</div>
<div>
<CardTitle className="mb-2 text-2xl">{feature.title}</CardTitle>
<CardDescription className="text-base">
{feature.description}
</CardDescription>
</div>
</CardHeader>
</Card>
Key patterns demonstrated:
- Using
classNameto extend base styles without modifying the component - Combining multiple shadcn primitives (Card + Badge)
- Adding custom elements (gradient divs) while preserving semantic structure
- Using Tailwind's
grouputilities for interactive effects
Component Composition Patterns
Using class-variance-authority (CVA)
shadcn-ui uses CVA for managing component variants. This is the standard pattern:
// From src/components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// Base styles (always applied)
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
Creating Custom Variants (Without Modifying components/ui)
If you need custom button variants, create a composed component:
// src/components/shared/GradientButton.tsx
import { forwardRef } from 'react';
import { Button, type ButtonProps } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export interface GradientButtonProps extends ButtonProps {
gradient?: 'blue' | 'purple' | 'pink';
}
export const GradientButton = forwardRef<HTMLButtonElement, GradientButtonProps>(
({ gradient = 'blue', className, ...props }, ref) => {
const gradients = {
blue: 'bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600',
purple: 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600',
pink: 'bg-gradient-to-r from-pink-500 to-rose-500 hover:from-pink-600 hover:to-rose-600',
};
return (
<Button
ref={ref}
className={cn(gradients[gradient], 'text-white shadow-lg', className)}
{...props}
/>
);
}
);
GradientButton.displayName = 'GradientButton';
Key patterns:
- Extends
ButtonPropsfor full type safety - Uses composition (wraps
Button) instead of modification - Uses
forwardReffor ref forwarding - Sets
displayNamefor better debugging
TypeScript Patterns
forwardRef with Proper Types
All shadcn components use React.forwardRef for ref forwarding:
// From src/components/ui/card.tsx
const Card = React.forwardRef<
HTMLDivElement, // Ref type
React.HTMLAttributes<HTMLDivElement> // Props type
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border bg-card text-card-foreground shadow',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
Type anatomy:
- First generic: Element type the ref points to
- Second generic: Props interface
- Always set
displayNamefor React DevTools
Extending Component Props
Use intersection types to extend base component props:
// From src/components/ui/badge.tsx
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
Benefits:
- Full HTML element props (onClick, aria-*, etc.)
- Type-safe variant props
- Intellisense for all valid props
The cn() Utility
What It Does
Located in src/lib/utils.ts:
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Purpose:
clsx: Conditionally combine classNamestwMerge: Intelligently merge Tailwind classes (later classes override earlier ones)
Usage Patterns
// Conditional classes
<Card className={cn(
'base-class',
isActive && 'active-class',
isDisabled && 'disabled-class',
className // Always pass through user className last
)} />
// Merging with base styles
<Button className={cn(
buttonVariants({ variant, size }), // Base variants
className // User overrides
)} />
// Complex conditions
<div className={cn(
'flex items-center',
orientation === 'vertical' ? 'flex-col' : 'flex-row',
spacing === 'lg' ? 'gap-4' : 'gap-2',
className
)} />
Always:
- Put user's
classNameprop last so they can override - Use
cn()instead of template literals for Tailwind classes
Accessibility-First Approach
Leveraging Radix UI Primitives
shadcn-ui is built on Radix UI, which provides accessible primitives out of the box.
Example from Button:
import { Slot } from '@radix-ui/react-slot';
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
asChild pattern:
- Allows Button to render as any element (e.g., Link)
- Preserves Button styles while changing semantics
- Maintains accessibility of the underlying element
// Button as a Link
<Button asChild>
<a href="/about">About</a>
</Button>
Always Spread Props
<div {...props} /> // Allows aria-*, role, data-* attributes
This ensures consumers can add accessibility attributes without modification.
Import Patterns
Barrel Exports
Components in components/ui/ use barrel exports:
// Good: Named exports allow tree-shaking
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
// Usage: Import only what you need
import { Card, CardHeader, CardTitle } from '@/components/ui/card';
Path Aliases
Use the @/ alias configured in tsconfig.json:
// Good
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
// Avoid
import { Card } from '../../../components/ui/card';
import { cn } from '../../lib/utils';
Do's and Don'ts
DO
- Use Card for layout and grouping related content
- Compose new components in
components/shared/when you need custom behavior - Use
classNameprop to extend styles - Use
cn()utility for combining classes - Spread
{...props}to allow consumer customization - Use
forwardRefwhen creating composed components - Export variant functions (e.g.,
buttonVariants) for reuse - Leverage CVA for managing variants
- Keep semantic HTML structure (CardHeader, CardContent, CardFooter)
DON'T
- Modify files in
components/ui/directly - Hardcode colors (use Tailwind's theme system)
- Skip forwardRef when composing components that need refs
- Forget to set
displayNameon forwardRef components - Use string concatenation for classNames (use
cn()instead) - Create one-off variants by modifying base components
- Skip TypeScript types (always type your props)
- Override Radix UI accessibility features without understanding them
Common Patterns from This Codebase
Pattern 1: Feature Cards with Icons
// Home.tsx pattern
const features = [
{
title: 'Feature Name',
badge: 'Badge Text',
icon: IconComponent,
description: 'Description text',
gradient: 'from-violet-500 to-purple-500',
},
];
<Card className="group relative overflow-hidden">
<CardHeader>
<div className="flex items-start justify-between">
<div className={`rounded-xl bg-gradient-to-br ${gradient} p-3`}>
<Icon className="h-6 w-6 text-white" />
</div>
<Badge>{badge}</Badge>
</div>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
</Card>
Pattern 2: Interactive Cards with Hover Effects
<Card className="group transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl">
{/* Use Tailwind group utilities for coordinated animations */}
<div className="transition-transform duration-300 group-hover:scale-110">
{/* Content */}
</div>
</Card>
Pattern 3: Grid Layouts with Cards
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{items.map((item) => (
<Card key={item.id}>
{/* Card content */}
</Card>
))}
</div>
Extending shadcn Components: Complete Example
Creating a custom StatCard component in components/shared/:
// src/components/shared/StatCard.tsx
import { forwardRef } from 'react';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { LucideIcon } from 'lucide-react';
export interface StatCardProps extends React.HTMLAttributes<HTMLDivElement> {
title: string;
value: string | number;
icon: LucideIcon;
trend?: {
value: number;
isPositive: boolean;
};
description?: string;
}
export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
({ title, value, icon: Icon, trend, description, className, ...props }, ref) => {
return (
<Card
ref={ref}
className={cn('transition-all hover:shadow-lg', className)}
{...props}
>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{trend && (
<Badge
variant={trend.isPositive ? 'default' : 'destructive'}
className="mt-2"
>
{trend.isPositive ? '+' : ''}
{trend.value}%
</Badge>
)}
{description && (
<p className="mt-2 text-xs text-muted-foreground">{description}</p>
)}
</CardContent>
</Card>
);
}
);
StatCard.displayName = 'StatCard';
Usage:
import { StatCard } from '@/components/shared/StatCard';
import { Users } from 'lucide-react';
<StatCard
title="Total Users"
value="2,543"
icon={Users}
trend={{ value: 12.5, isPositive: true }}
description="Up from last month"
/>
Summary Checklist
When working with shadcn-ui in this project:
- Need to modify a component? Create a new one in
components/shared/ - Creating a composed component? Use
forwardRefand setdisplayName - Need custom variants? Use CVA pattern or className composition
- Combining classes? Use the
cn()utility - Need a container? Consider using Card with semantic sub-components
- Creating interactive elements? Use Tailwind's
grouputilities - Need type safety? Extend base component props with intersection types
- Importing? Use
@/path alias and named imports - Adding functionality? Spread
{...props}to preserve flexibility - Updating shadcn components? Use CLI, never manual edits to
components/ui/
Resources
More by JewelsHovan
View allAggregate and analyze AI news from 7 authoritative sources including expert newsletters (Andrew Ng's The Batch), research papers (HuggingFace), industry news (TechCrunch, AI News), and community discussions (Reddit, Hacker News). Provides deep trend analysis with expert sentiment and community opinions. This skill should be used when the user wants a comprehensive AI news digest, research recent developments, understand community sentiment, or stay updated on AI trends. Invoke with `/ai-news <days>` (e.g., `/ai-news 3` for past 3 days).
Standardize page structure - always use MainLayout wrapper, space-y-32 for sections, px-8 py-16 for page padding, responsive grid patterns
Tailwind CSS v4 styling guidelines - use @theme blocks for configuration, hsl(var(--color-x)) for colors, never hardcode values, use responsive utilities
Maintain consistent file structure - components/ui for base shadcn, components/shared for composed components, pages for routes, always use barrel exports
