Claude Code Plugins

Community-maintained marketplace

Feedback
2
0

Add a new component following Next.js 15 and shadcn/ui best practices

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 Add Component
description Add a new component following Next.js 15 and shadcn/ui best practices

Add Component Skill

You are helping create a new component in a Next.js 15 application with TypeScript, Tailwind CSS, and shadcn/ui.

Context

  • Framework: Next.js 15 with App Router
  • Styling: Tailwind CSS v4 + shadcn/ui
  • TypeScript: Strict mode
  • Component Library: shadcn/ui (Radix UI primitives)

Decision: Server or Client Component?

Ask First

Before creating any component, determine if it needs to be a Client Component.

Use Server Component (default) if:

  • No interactivity needed
  • No React hooks (useState, useEffect, etc.)
  • No browser APIs
  • Just rendering data

Use Client Component only if:

  • Has onClick, onChange, or other event handlers
  • Uses React hooks (useState, useEffect, useContext)
  • Uses browser APIs (localStorage, window, etc.)
  • Needs real-time updates

Component Types

1. UI Component (shadcn/ui style)

For reusable UI primitives (buttons, inputs, cards, etc.):

// components/ui/[component-name].tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const componentVariants = cva(
  "base-classes-here",
  {
    variants: {
      variant: {
        default: "default-classes",
        secondary: "secondary-classes",
      },
      size: {
        default: "default-size-classes",
        sm: "small-size-classes",
        lg: "large-size-classes",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ComponentProps
  extends React.HTMLAttributes<HTMLElement>,
    VariantProps<typeof componentVariants> {
  // Custom props here
}

const Component = React.forwardRef<HTMLElement, ComponentProps>(
  ({ className, variant, size, ...props }, ref) => {
    return (
      <element
        className={cn(componentVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Component.displayName = "Component"

export { Component, componentVariants }

2. Server Component (Default)

For components that just render data:

// components/[feature]/[component-name].tsx
interface ComponentProps {
  data: DataType
  className?: string
}

export function Component({ data, className }: ComponentProps) {
  return (
    <div className={className}>
      {/* Render data */}
    </div>
  )
}

3. Client Component

For interactive components:

// components/[feature]/[component-name].tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"

interface ComponentProps {
  initialValue?: string
  onSubmit?: (value: string) => void
}

export function Component({ initialValue = "", onSubmit }: ComponentProps) {
  const [value, setValue] = useState(initialValue)

  function handleSubmit() {
    onSubmit?.(value)
  }

  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <Button onClick={handleSubmit}>Submit</Button>
    </div>
  )
}

4. Form Component (with Server Action)

// components/[feature]/[form-name].tsx
"use client"

import { useState } from "react"
import { useRouter } from "next/navigation"
import { serverAction } from "@/lib/actions/[feature]"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function FormComponent() {
  const router = useRouter()
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setIsLoading(true)
    setError(null)

    const formData = new FormData(e.currentTarget)
    const result = await serverAction({
      field: formData.get("field") as string,
    })

    if (result.success) {
      router.push("/success")
      router.refresh()
    } else {
      setError(result.error)
    }

    setIsLoading(false)
  }

  return (
    <form onSubmit={onSubmit} className="space-y-4">
      {error && (
        <div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
          {error}
        </div>
      )}

      <div className="space-y-2">
        <Label htmlFor="field">Field Label</Label>
        <Input
          id="field"
          name="field"
          type="text"
          required
          disabled={isLoading}
        />
      </div>

      <Button type="submit" disabled={isLoading}>
        {isLoading ? "Submitting..." : "Submit"}
      </Button>
    </form>
  )
}

Styling Guidelines

Use Tailwind Classes

// ✅ Good - Tailwind utilities
<div className="flex items-center gap-4 rounded-lg border bg-card p-6">
  <h2 className="text-2xl font-bold">Title</h2>
</div>

// ❌ Bad - Inline styles
<div style={{ display: 'flex', padding: '24px' }}>
  <h2 style={{ fontSize: '24px' }}>Title</h2>
</div>

Use cn() for Conditional Classes

import { cn } from "@/lib/utils"

<div className={cn(
  "rounded-lg border p-4",
  isActive && "bg-primary text-primary-foreground",
  isDisabled && "opacity-50 cursor-not-allowed"
)} />

Use Design System Colors

// ✅ Good - Use CSS variables
<div className="bg-background text-foreground border-border">
  <h1 className="text-primary">Title</h1>
  <p className="text-muted-foreground">Description</p>
</div>

// ❌ Bad - Hard-coded colors
<div className="bg-white text-black border-gray-200">
  <h1 className="text-blue-600">Title</h1>
</div>

TypeScript Guidelines

Define Explicit Props

// ✅ Good - Explicit interface
interface UserCardProps {
  user: {
    id: string
    name: string
    email: string
  }
  showEmail?: boolean
  onEdit?: (userId: string) => void
  className?: string
}

// ❌ Bad - Implicit any
function UserCard(props) {
  // ...
}

Extend HTML Attributes

// For native HTML elements
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "default" | "destructive"
  isLoading?: boolean
}

// For divs
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  title: string
}

Use forwardRef for Reusable Components

import * as React from "react"

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: string
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, error, ...props }, ref) => {
    return (
      <div>
        <input ref={ref} className={className} {...props} />
        {error && <p className="text-sm text-destructive">{error}</p>}
      </div>
    )
  }
)
Input.displayName = "Input"

export { Input }

Composition Pattern

Pass Server Components to Client Components as children:

// app/page.tsx (Server Component)
import { ClientWrapper } from "@/components/client-wrapper"
import { ServerData } from "@/components/server-data"

export default async function Page() {
  return (
    <ClientWrapper>
      <ServerData />  {/* Server Component as child */}
    </ClientWrapper>
  )
}

// components/client-wrapper.tsx (Client Component)
"use client"

export function ClientWrapper({ children }: { children: React.ReactNode }) {
  return <div className="interactive-wrapper">{children}</div>
}

// components/server-data.tsx (Server Component)
export async function ServerData() {
  const data = await fetchData()
  return <div>{data}</div>
}

Common Patterns

Card Component

import { cn } from "@/lib/utils"

interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  title?: string
  description?: string
}

export function Card({
  title,
  description,
  className,
  children,
  ...props
}: CardProps) {
  return (
    <div
      className={cn(
        "rounded-lg border bg-card text-card-foreground shadow-sm",
        className
      )}
      {...props}
    >
      {(title || description) && (
        <div className="border-b p-6">
          {title && <h3 className="text-lg font-semibold">{title}</h3>}
          {description && (
            <p className="text-sm text-muted-foreground">{description}</p>
          )}
        </div>
      )}
      <div className="p-6">{children}</div>
    </div>
  )
}

Loading Skeleton

export function ComponentSkeleton() {
  return (
    <div className="space-y-4">
      <div className="h-8 w-64 animate-pulse rounded bg-muted" />
      <div className="h-4 w-96 animate-pulse rounded bg-muted" />
      <div className="h-4 w-80 animate-pulse rounded bg-muted" />
    </div>
  )
}

Empty State

import { FileX } from "lucide-react"

interface EmptyStateProps {
  title: string
  description?: string
  action?: React.ReactNode
}

export function EmptyState({ title, description, action }: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center p-8 text-center">
      <FileX className="h-12 w-12 text-muted-foreground" />
      <h3 className="mt-4 text-lg font-semibold">{title}</h3>
      {description && (
        <p className="mt-2 text-sm text-muted-foreground">{description}</p>
      )}
      {action && <div className="mt-4">{action}</div>}
    </div>
  )
}

Accessibility

Always include proper accessibility attributes:

// Labels for inputs
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" />

// ARIA attributes
<button aria-label="Close dialog" onClick={onClose}>
  <X className="h-4 w-4" />
</button>

// Semantic HTML
<article>
  <h1>Title</h1>
  <p>Content</p>
</article>

File Organization

components/
├── ui/                     # shadcn/ui components
│   ├── button.tsx
│   ├── input.tsx
│   └── card.tsx
├── auth/                   # Auth-specific components
│   ├── signin-form.tsx
│   └── signup-form.tsx
├── dashboard/              # Dashboard components
│   ├── stats-card.tsx
│   └── nav.tsx
└── shared/                 # Shared across features
    ├── header.tsx
    └── footer.tsx

Checklist

Before completing, ensure:

  • Correct component type (Server vs Client)
  • TypeScript interfaces defined
  • Props properly typed
  • Tailwind classes used (no inline styles)
  • Responsive design (mobile-first)
  • Accessibility attributes
  • Error states handled
  • Loading states (if async)
  • Exported correctly
  • Documented if complex

Now help the user create their component!