--- name: component-development description: Patterns and best practices for developing React 19 components with Shadcn UI, Radix primitives, and Tailwind CSS 4 in Next.js 16. Covers accessibility, composition, variants, and server/client component patterns. Use when creating, modifying, or debugging UI components. --- # Component Development Skill Comprehensive guide for building accessible, composable React 19 components using Shadcn UI, Radix primitives, and Tailwind CSS 4 in The Simpsons API project. ## When to Use This Skill Use this skill when the user requests: ✅ **Primary Use Cases** - "Create a component" - "Build a UI element" - "Add Shadcn component" - "Make this accessible" - "Add dark mode support" - "Create reusable component" ✅ **Secondary Use Cases** - "Fix component styling" - "Add component variants" - "Make responsive component" - "Compose components together" - "Debug rendering issues" - "Add animations/transitions" ❌ **Do NOT use when** - Server-only data fetching (use repositories) - Server actions (use server-actions-patterns skill) - Global styles (use globals.css directly) - Configuration changes (use project docs) ## Project Context ### Component Structure ``` app/_components/ # App-specific components ├── CharacterImage.tsx ├── CommentSection.tsx ├── CreateCollectionForm.tsx ├── DeleteDiaryEntryButton.tsx ├── DiaryForm.tsx ├── EpisodeTracker.tsx ├── FollowButton.tsx ├── IntroSection.tsx ├── RecentlyViewedList.tsx ├── RecentlyViewedTracker.tsx ├── SimpsonsHeader.tsx ├── SyncButton.tsx └── TriviaSection.tsx components/ui/ # Shadcn UI primitives ├── avatar.tsx ├── badge.tsx ├── button.tsx ├── card.tsx ├── input.tsx ├── label.tsx ├── select.tsx └── textarea.tsx ``` ### Tech Stack - **React**: 19 (with use client, useOptimistic, useActionState) - **Styling**: Tailwind CSS 4 (`@import "tailwindcss"` syntax) - **UI Library**: Shadcn UI (installed via `pnpm dlx shadcn@latest add`) - **Primitives**: Radix UI (via Shadcn) - **Icons**: Lucide React (recommended) --- ## Core Patterns ### Pattern 1: Server Component (Default) ```tsx // app/_components/CharacterCard.tsx // No "use client" = Server Component by default import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { DBCharacter } from "@/app/_lib/db-types"; interface CharacterCardProps { character: DBCharacter; } export function CharacterCard({ character }: CharacterCardProps) { return ( {character.name} {character.is_main && ( Main )}

{character.occupation}

{character.catchphrase && (

"{character.catchphrase}"

)}
); } ``` ### Pattern 2: Client Component with Interactivity ```tsx // app/_components/FollowButton.tsx "use client"; import { useState, useTransition } from "react"; import { Button } from "@/components/ui/button"; import { Heart, HeartOff, Loader2 } from "lucide-react"; import { toggleFollow } from "@/app/_actions/social"; interface FollowButtonProps { characterId: number; initialFollowing: boolean; className?: string; } export function FollowButton({ characterId, initialFollowing, className, }: FollowButtonProps) { const [isFollowing, setIsFollowing] = useState(initialFollowing); const [isPending, startTransition] = useTransition(); async function handleClick() { startTransition(async () => { // Optimistic update setIsFollowing(!isFollowing); const result = await toggleFollow(characterId); if (!result.success) { // Revert on error setIsFollowing(isFollowing); console.error(result.error); } }); } return ( ); } ``` ### Pattern 3: Component with Variants (CVA) ```tsx // components/ui/status-badge.tsx import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const statusBadgeVariants = cva( "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors", { variants: { status: { success: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100", warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100", error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100", info: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100", neutral: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100", }, size: { sm: "text-xs px-2 py-0.5", md: "text-sm px-2.5 py-0.5", lg: "text-base px-3 py-1", }, }, defaultVariants: { status: "neutral", size: "md", }, } ); interface StatusBadgeProps extends React.HTMLAttributes, VariantProps { children: React.ReactNode; } export function StatusBadge({ status, size, className, children, ...props }: StatusBadgeProps) { return ( {children} ); } // Usage: // Active // Failed ``` ### Pattern 4: Compound Component Pattern ```tsx // components/ui/character-profile.tsx "use client"; import { createContext, useContext } from "react"; import { cn } from "@/lib/utils"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; // Context for compound components const CharacterProfileContext = createContext<{ name: string; imageUrl?: string; } | null>(null); function useCharacterProfile() { const context = useContext(CharacterProfileContext); if (!context) { throw new Error("CharacterProfile components must be used within CharacterProfile"); } return context; } // Root component interface CharacterProfileProps { name: string; imageUrl?: string; children: React.ReactNode; className?: string; } function CharacterProfile({ name, imageUrl, children, className }: CharacterProfileProps) { return (
{children}
); } // Sub-components function ProfileAvatar({ className }: { className?: string }) { const { name, imageUrl } = useCharacterProfile(); return ( {imageUrl && } {name.slice(0, 2).toUpperCase()} ); } function ProfileName({ className }: { className?: string }) { const { name } = useCharacterProfile(); return

{name}

; } function ProfileContent({ children, className }: { children: React.ReactNode; className?: string }) { return
{children}
; } // Attach sub-components CharacterProfile.Avatar = ProfileAvatar; CharacterProfile.Name = ProfileName; CharacterProfile.Content = ProfileContent; export { CharacterProfile }; // Usage: // // // // //

Safety Inspector at Springfield Nuclear Power Plant

//
//
``` ### Pattern 5: Polymorphic Component (as prop) ```tsx // components/ui/box.tsx import { cn } from "@/lib/utils"; import { type ElementType, type ComponentPropsWithoutRef } from "react"; type BoxProps = { as?: T; className?: string; children?: React.ReactNode; } & Omit, "as" | "className" | "children">; export function Box({ as, className, children, ...props }: BoxProps) { const Component = as || "div"; return ( {children} ); } // Usage: // Default div // Section element // Article element // Link element ``` --- ## Shadcn UI Integration ### Installing New Components ```bash # Add single component pnpm dlx shadcn@latest add button # Add multiple components pnpm dlx shadcn@latest add card badge avatar # List available components pnpm dlx shadcn@latest add ``` ### Customizing Shadcn Components ```tsx // Extend the button with Simpsons theme // components/ui/simpsons-button.tsx import { Button, type ButtonProps } from "@/components/ui/button"; import { cn } from "@/lib/utils"; interface SimpsonsButtonProps extends ButtonProps { character?: "homer" | "bart" | "lisa" | "marge"; } const characterColors = { homer: "bg-amber-500 hover:bg-amber-600 text-white", bart: "bg-orange-500 hover:bg-orange-600 text-white", lisa: "bg-red-500 hover:bg-red-600 text-white", marge: "bg-blue-500 hover:bg-blue-600 text-white", }; export function SimpsonsButton({ character, className, variant, ...props }: SimpsonsButtonProps) { return ( {/* Live region announces changes to screen readers */} {count} ); } ``` ### Focus Management ```tsx "use client"; import { useEffect, useRef } from "react"; export function AutoFocusInput({ shouldFocus }: { shouldFocus: boolean }) { const inputRef = useRef(null); useEffect(() => { if (shouldFocus && inputRef.current) { inputRef.current.focus(); } }, [shouldFocus]); return ( ); } ``` --- ## Dark Mode Support ### Using CSS Variables (Tailwind CSS 4) ```tsx // Component automatically adapts to dark mode via CSS variables export function ThemedCard({ children }: { children: React.ReactNode }) { return (
{children}
); } // bg-background = var(--background) // text-foreground = var(--foreground) // These switch automatically with .dark class on html ``` ### Manual Dark Mode Variants ```tsx export function DarkModeExample() { return (

Title

This adapts to dark mode automatically.

Divider also adapts
); } ``` --- ## Animation Patterns ### Tailwind Animations ```tsx // Entrance animation
Content appears with animation
// Exit animation
Content exits with animation
// Spin animation // Pulse animation
// Bounce animation 👋 ``` ### CSS Transitions ```tsx export function HoverCard({ children }: { children: React.ReactNode }) { return (
{children} {/* Hidden content that appears on hover */}
Click for more
); } ``` ### Framer Motion Integration ```tsx "use client"; import { motion, AnimatePresence } from "framer-motion"; export function AnimatedList({ items }: { items: string[] }) { return (
    {items.map((item, index) => ( {item} ))}
); } ``` --- ## Responsive Design ### Mobile-First Approach ```tsx export function ResponsiveGrid({ children }: { children: React.ReactNode }) { return (
{children}
); } ``` ### Container Queries (Tailwind CSS 4) ```tsx export function ContainerQueryCard() { return (

Title

Content adapts to container

); } ``` ### Breakpoint Reference | Prefix | Min Width | Typical Use | |--------|-----------|-------------| | `sm:` | 640px | Large phones | | `md:` | 768px | Tablets | | `lg:` | 1024px | Laptops | | `xl:` | 1280px | Desktops | | `2xl:` | 1536px | Large screens | --- ## Common Mistakes ### ❌ Wrong: Using hooks in Server Component ```tsx // This will error import { useState } from "react"; export function ServerComponent() { const [state, setState] = useState(false); // ERROR return
{state}
; } ``` ### ✅ Correct: Add "use client" or move to client component ```tsx "use client"; import { useState } from "react"; export function ClientComponent() { const [state, setState] = useState(false); return
{state}
; } ``` ### ❌ Wrong: Passing functions to client components ```tsx // page.tsx (Server Component) export default function Page() { function handleClick() { // Server function console.log("clicked"); } return ; // ERROR } ``` ### ✅ Correct: Use server actions or client-side handlers ```tsx // Option 1: Server Action "use server"; export async function handleClick() { console.log("clicked on server"); } // Option 2: Define handler in client component "use client"; export function ClientButton() { function handleClick() { console.log("clicked on client"); } return ; } ``` --- ## Related Skills - [server-actions-patterns](../server-actions-patterns/SKILL.md) - Form handling with actions - [webapp-testing](../webapp-testing/SKILL.md) - Testing components - [performance-optimization](../performance-optimization/SKILL.md) - Component performance --- ## References - [Shadcn UI Documentation](https://ui.shadcn.com/) - [Radix UI Primitives](https://www.radix-ui.com/) - [Tailwind CSS Documentation](https://tailwindcss.com/docs) - [React 19 Documentation](https://react.dev/) - [Next.js App Router](https://nextjs.org/docs/app) --- **Last Updated:** January 14, 2026 **Maintained By:** Development Team **Status:** ✅ Production Ready