Claude Code Plugins

Community-maintained marketplace

Feedback

shadcn-ui-best-practices

@JewelsHovan/pain-plus-site
0
0

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

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 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:

// 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 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:

// 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:

// 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 classNames
  • twMerge: 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 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:

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 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

// 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 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