AgentSkillsCN

Shadcn Ui

Shadcn UI

SKILL.md
skill
---
name: shadcn-ui
description: shadcn/ui component library patterns, best practices, and MCP integration. Use when building UI with shadcn/ui components, customizing components, or using the shadcn MCP server. Triggers on tasks involving component installation, UI composition, theming, or when explicitly asked about shadcn patterns.
license: MIT
metadata:
  author: beth-team
  version: "1.0.0"
  registry: https://ui.shadcn.com
---

# shadcn/ui Component Skill

This skill covers shadcn/ui component patterns, installation, customization, and MCP server integration for AI-assisted development.

## What is shadcn/ui?

shadcn/ui is **not a component library**—it's a code distribution system. You own the code. Components are copied into your project where you can customize them freely.

Key principles:
- **Open Code**: You get the actual component source code
- **Composition**: Components share a consistent, composable API
- **Beautiful Defaults**: Carefully designed defaults that work together
- **AI-Ready**: Open code for LLMs to read, understand, and modify

## MCP Server Integration

The shadcn MCP server enables AI assistants to interact directly with component registries.

### Configuration

**VS Code (GitHub Copilot):**
```bash
npx shadcn@latest mcp init --client vscode
```

Creates `.vscode/mcp.json`:
```json
{
  "servers": {
    "shadcn": {
      "command": "npx",
      "args": ["shadcn@latest", "mcp"]
    }
  }
}
```

**Other Clients:**
- Claude Code: `npx shadcn@latest mcp init --client claude`
- Cursor: `npx shadcn@latest mcp init --client cursor`
- Codex: Manual config in `~/.codex/config.toml`

### MCP Tools

| Tool | Purpose | Example Use |
|------|---------|-------------|
| `get_project_registries` | List configured registries | Verify setup |
| `search_items_in_registries` | Search by name/function | "Find data table component" |
| `list_items_in_registries` | Browse all components | Explore options |
| `get_item_details_from_registries` | Get full source code | Understand implementation |
| `get_item_examples_from_registries` | Get usage examples | Learn patterns |
| `add_items_to_project` | Install components | "Add button and card" |

### Example MCP Prompts

```
# Discovery
"Show me all available components in the shadcn registry"
"What form components are available?"
"Find me a date picker"

# Details
"Show me the code for the dialog component"
"Get examples of the data-table component"
"What are the variants for the button component?"

# Installation
"Add button, card, and dialog to this project"
"Install all the form components"
"Add the chart component"
```

## Project Setup

### Initialize a new project

```bash
npx shadcn@latest init
```

This creates:
- `components.json` - Configuration
- `lib/utils.ts` - Utility functions (cn helper)
- `tailwind.config.ts` - Tailwind configuration
- `globals.css` - CSS variables for theming

### Configuration (components.json)

```json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  },
  "registries": {
    "@shadcn": "https://ui.shadcn.com/r/{name}.json"
  }
}
```

### Add components

```bash
# Single component
npx shadcn@latest add button

# Multiple components
npx shadcn@latest add button card dialog

# All components
npx shadcn@latest add --all
```

## Component Patterns

### Basic Usage

```tsx
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export function Example() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Hello World</CardTitle>
      </CardHeader>
      <CardContent>
        <Button>Click Me</Button>
      </CardContent>
    </Card>
  );
}
```

### Composition Pattern

shadcn/ui components are composable by design:

```tsx
// Dialog with custom trigger
<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline">Open Settings</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Settings</DialogTitle>
      <DialogDescription>Configure your preferences.</DialogDescription>
    </DialogHeader>
    {/* Content */}
    <DialogFooter>
      <Button type="submit">Save</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

### The cn() Utility

The `cn()` helper merges Tailwind classes intelligently:

```tsx
import { cn } from "@/lib/utils";

function MyComponent({ className, ...props }) {
  return (
    <div className={cn("base-styles", className)} {...props} />
  );
}
```

Implementation (uses clsx + tailwind-merge):
```ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

### Component Variants with cva

Use `class-variance-authority` for component variants:

```tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const badgeVariants = cva(
  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
  {
    variants: {
      variant: {
        default: "border-transparent bg-primary text-primary-foreground",
        secondary: "border-transparent bg-secondary text-secondary-foreground",
        destructive: "border-transparent bg-destructive text-destructive-foreground",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
);

interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  );
}
```

## Theming

### CSS Variables

shadcn/ui uses CSS variables for theming:

```css
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  /* ... more variables */
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... dark mode overrides */
}
```

### Using Theme Colors

```tsx
// In Tailwind classes
<div className="bg-background text-foreground">
  <button className="bg-primary text-primary-foreground">
    Primary Button
  </button>
</div>

// Accessing in JS
const styles = {
  backgroundColor: 'hsl(var(--background))',
  color: 'hsl(var(--foreground))',
};
```

### Dark Mode

Configure dark mode in `tailwind.config.ts`:

```ts
module.exports = {
  darkMode: ["class"],
  // ...
}
```

Toggle with a theme provider:

```tsx
'use client';

import { ThemeProvider } from "next-themes";

export function Providers({ children }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}
```

## Form Patterns

### With React Hook Form + Zod

```tsx
'use client';

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export function LoginForm() {
  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: { email: "", password: "" },
  });

  async function onSubmit(values: z.infer<typeof schema>) {
    // Handle submission
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Login</Button>
      </form>
    </Form>
  );
}
```

## Data Display

### DataTable Pattern

```tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

interface Column<T> {
  header: string;
  accessor: keyof T | ((row: T) => React.ReactNode);
}

interface DataTableProps<T> {
  columns: Column<T>[];
  data: T[];
}

export function DataTable<T extends { id: string }>({ 
  columns, 
  data 
}: DataTableProps<T>) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          {columns.map((col, i) => (
            <TableHead key={i}>{col.header}</TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {data.map((row) => (
          <TableRow key={row.id}>
            {columns.map((col, i) => (
              <TableCell key={i}>
                {typeof col.accessor === 'function' 
                  ? col.accessor(row) 
                  : String(row[col.accessor])}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

## Accessibility

shadcn/ui components are built on Radix UI primitives which handle:

- Keyboard navigation
- Focus management
- ARIA attributes
- Screen reader announcements

### Ensure Accessibility in Custom Code

```tsx
// Always provide accessible labels
<Button aria-label="Close dialog">
  <X className="h-4 w-4" />
</Button>

// Use sr-only for screen reader text
<span className="sr-only">Loading</span>

// Handle focus management
<Dialog onOpenChange={(open) => {
  if (!open) {
    // Return focus to trigger
    triggerRef.current?.focus();
  }
}}>
```

## Best Practices

### DO

- ✅ Accept `className` prop for composition
- ✅ Use `cn()` to merge classes
- ✅ Forward refs when needed
- ✅ Keep components focused and composable
- ✅ Use TypeScript for props
- ✅ Add loading/disabled states

### DON'T

- ❌ Modify components in `ui/` directly for one-off changes
- ❌ Override styles with `!important`
- ❌ Create wrapper components when composition works
- ❌ Skip accessibility testing
- ❌ Use deprecated patterns

### When to Customize vs Compose

**Compose** (use as-is with props):
```tsx
<Button size="lg" variant="outline">Large Outline</Button>
```

**Extend** (create specialized version):
```tsx
// components/IconButton.tsx
import { Button, ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";

interface IconButtonProps extends ButtonProps {
  icon: React.ReactNode;
}

export function IconButton({ icon, className, ...props }: IconButtonProps) {
  return (
    <Button className={cn("p-2", className)} {...props}>
      {icon}
    </Button>
  );
}
```

**Customize** (modify source when pattern doesn't exist):
- Fork `ui/button.tsx` only for project-wide changes
- Document the customization

## Common Components Reference

| Component | Use For |
|-----------|---------|
| `Button` | Actions, form submissions |
| `Card` | Content containers |
| `Dialog` | Modal interactions |
| `Sheet` | Side panels |
| `Form` | Form containers with validation |
| `Input`, `Textarea` | Text inputs |
| `Select` | Dropdowns |
| `Checkbox`, `Radio`, `Switch` | Toggles |
| `Table` | Data display |
| `Tabs` | Content organization |
| `Toast` | Notifications |
| `Tooltip` | Contextual info |
| `Command` | Command palettes |
| `DropdownMenu` | Action menus |
| `NavigationMenu` | Site navigation |
| `Avatar` | User images |
| `Badge` | Status indicators |
| `Skeleton` | Loading placeholders |

## Troubleshooting

### Component not found
```bash
# Check available components
npx shadcn@latest add --help

# Search via MCP
"List all components in the shadcn registry"
```

### Styling conflicts
- Ensure Tailwind is properly configured
- Check CSS variable definitions
- Use `cn()` for class merging

### Dark mode not working
- Verify `darkMode: ["class"]` in tailwind.config
- Check ThemeProvider is wrapping your app
- Ensure CSS variables have dark mode overrides

### TypeScript errors
- Run `npx shadcn@latest add [component]` to get latest types
- Check component is exported from `ui/` index