# Shadcn UI Best Practices

> Guide for proper shadcn-ui component usage - use Card for wrapping/layout, compose from base components, never modify components/ui directly

- Skill: `majiayu000/shadcn-ui-best-practices` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/shadcn-ui-best-practices`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/shadcn-ui-best-practices/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/shadcn-ui-best-practices

---


# 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:

```tsx
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

```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 `className` to 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 `group` utilities for interactive effects

---

## Component Composition Patterns

### Using class-variance-authority (CVA)

shadcn-ui uses CVA for managing component variants. This is the standard pattern:

```tsx
// 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:

```tsx
// 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 `ButtonProps` for full type safety
- Uses composition (wraps `Button`) instead of modification
- Uses `forwardRef` for ref forwarding
- Sets `displayName` for better debugging

---

## TypeScript Patterns

### forwardRef with Proper Types

All shadcn components use React.forwardRef for ref forwarding:

```tsx
// 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**:
1. First generic: Element type the ref points to
2. Second generic: Props interface
3. Always set `displayName` for React DevTools

### Extending Component Props

Use intersection types to extend base component props:

```tsx
// 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`:

```tsx
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

**Purpose**:
- `clsx`: Conditionally combine classNames
- `twMerge`: Intelligently merge Tailwind classes (later classes override earlier ones)

### Usage Patterns

```tsx
// 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 `className` prop 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**:
```tsx
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

```tsx
// Button as a Link
<Button asChild>
  <a href="/about">About</a>
</Button>
```

### Always Spread Props

```tsx
<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:

```tsx
// 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:

```tsx
// 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 `className` prop to extend styles
- Use `cn()` utility for combining classes
- Spread `{...props}` to allow consumer customization
- Use `forwardRef` when 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 `displayName` on 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

```tsx
// 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

```tsx
<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

```tsx
<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/`:

```tsx
// 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**:

```tsx
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 `forwardRef` and set `displayName`
- [ ] 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 `group` utilities
- [ ] 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

- [shadcn-ui Documentation](https://ui.shadcn.com)
- [Radix UI Primitives](https://www.radix-ui.com)
- [CVA Documentation](https://cva.style/docs)
- [Tailwind CSS](https://tailwindcss.com)

