modelscope.cn

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

Installation

$ npx skills add https://modelscope.cn

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from modelscope.cn · top by installs.

npx skills add https://modelscope.cn

Browse all from modelscope.cn

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 15,408 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

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