COMPONENTS.md 12 KB

Component Patterns Library

Common UI component patterns with Tailwind CSS + Shadcn/ui examples.


Table of Contents

  1. Buttons
  2. Cards
  3. Forms
  4. Navigation
  5. Modals & Dialogs
  6. Data Display
  7. File Upload
  8. Calendar & Date Pickers
  9. Notifications & Toasts
  10. Loading States

Buttons

Primary Button

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

<Button className="bg-blue-600 hover:bg-blue-700">
  Click Me
</Button>

Button Sizes

<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>

Button Variants

<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>

Loading Button

<Button disabled>
  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  Processing...
</Button>

Cards

Basic Card

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"

<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Card description goes here</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Card content</p>
  </CardContent>
</Card>

Card with Footer

<Card>
  <CardHeader>
    <CardTitle>Settings</CardTitle>
  </CardHeader>
  <CardContent>
    <p>Configure your preferences</p>
  </CardContent>
  <CardFooter className="flex justify-between">
    <Button variant="ghost">Cancel</Button>
    <Button>Save</Button>
  </CardFooter>
</Card>

Hover Card (Interactive)

<Card className="transition-all hover:shadow-lg hover:scale-105">
  <CardHeader>
    <CardTitle>Hover Me</CardTitle>
  </CardHeader>
  <CardContent>
    <p>This card responds to hover</p>
  </CardContent>
</Card>

Forms

Input Field

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

<div className="space-y-2">
  <Label htmlFor="email">Email</Label>
  <Input 
    id="email" 
    type="email" 
    placeholder="you@example.com"
  />
</div>

Textarea

import { Textarea } from "@/components/ui/textarea"

<div className="space-y-2">
  <Label htmlFor="message">Message</Label>
  <Textarea 
    id="message" 
    placeholder="Type your message here" 
    rows={4}
  />
</div>

Select Dropdown

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"

<Select>
  <SelectTrigger className="w-[180px]">
    <SelectValue placeholder="Select option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
    <SelectItem value="option3">Option 3</SelectItem>
  </SelectContent>
</Select>

Checkbox

import { Checkbox } from "@/components/ui/checkbox"

<div className="flex items-center space-x-2">
  <Checkbox id="terms" />
  <Label htmlFor="terms">Accept terms and conditions</Label>
</div>

Radio Group

import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

<RadioGroup defaultValue="option1">
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="option1" id="option1" />
    <Label htmlFor="option1">Option 1</Label>
  </div>
  <div className="flex items-center space-x-2">
    <RadioGroupItem value="option2" id="option2" />
    <Label htmlFor="option2">Option 2</Label>
  </div>
</RadioGroup>

Navigation

Horizontal Nav

<nav className="flex items-center justify-between px-6 py-4 border-b">
  <div className="text-2xl font-bold">Logo</div>
  <div className="flex gap-6">
    <a href="/home" className="hover:text-blue-600 transition-colors">Home</a>
    <a href="/about" className="hover:text-blue-600 transition-colors">About</a>
    <a href="/contact" className="hover:text-blue-600 transition-colors">Contact</a>
  </div>
  <Button>Sign In</Button>
</nav>

Sidebar Navigation

<aside className="w-64 h-screen border-r p-6">
  <div className="text-xl font-bold mb-8">Dashboard</div>
  <nav className="space-y-2">
    <a href="#" className="flex items-center gap-3 px-4 py-2 rounded-lg hover:bg-gray-100">
      <HomeIcon className="w-5 h-5" />
      <span>Home</span>
    </a>
    <a href="#" className="flex items-center gap-3 px-4 py-2 rounded-lg hover:bg-gray-100">
      <SettingsIcon className="w-5 h-5" />
      <span>Settings</span>
    </a>
  </nav>
</aside>

Tabs

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

<Tabs defaultValue="tab1">
  <TabsList>
    <TabsTrigger value="tab1">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2">Tab 2</TabsTrigger>
    <TabsTrigger value="tab3">Tab 3</TabsTrigger>
  </TabsList>
  <TabsContent value="tab1">Tab 1 content</TabsContent>
  <TabsContent value="tab2">Tab 2 content</TabsContent>
  <TabsContent value="tab3">Tab 3 content</TabsContent>
</Tabs>

Modals & Dialogs

Basic Dialog

import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"

<Dialog>
  <DialogTrigger asChild>
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Are you sure?</DialogTitle>
      <DialogDescription>
        This action cannot be undone.
      </DialogDescription>
    </DialogHeader>
    <div className="flex justify-end gap-2 mt-4">
      <Button variant="ghost">Cancel</Button>
      <Button variant="destructive">Confirm</Button>
    </div>
  </DialogContent>
</Dialog>

Alert Dialog (Confirmation)

import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"

<AlertDialog>
  <AlertDialogTrigger asChild>
    <Button variant="destructive">Delete Account</Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. This will permanently delete your account.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Continue</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Data Display

Table

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

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Name</TableHead>
      <TableHead>Email</TableHead>
      <TableHead>Status</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>John Doe</TableCell>
      <TableCell>john@example.com</TableCell>
      <TableCell>Active</TableCell>
    </TableRow>
  </TableBody>
</Table>

Badge

import { Badge } from "@/components/ui/badge"

<Badge>New</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Error</Badge>

Avatar

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

<Avatar>
  <AvatarImage src="https://github.com/shadcn.png" />
  <AvatarFallback>CN</AvatarFallback>
</Avatar>

File Upload

Drag & Drop Zone

"use client"

import { useState } from "react"
import { Upload } from "lucide-react"

export function FileUpload() {
  const [isDragging, setIsDragging] = useState(false)
  
  return (
    <div
      onDragOver={(e) => {
        e.preventDefault()
        setIsDragging(true)
      }}
      onDragLeave={() => setIsDragging(false)}
      onDrop={(e) => {
        e.preventDefault()
        setIsDragging(false)
        // Handle file drop
      }}
      className={`
        border-2 border-dashed rounded-lg p-12 text-center
        transition-colors cursor-pointer
        ${isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}
      `}
    >
      <Upload className="w-12 h-12 mx-auto mb-4 text-gray-400" />
      <p className="text-lg font-medium">Drop files here or click to upload</p>
      <p className="text-sm text-gray-500 mt-2">PNG, JPG, GIF up to 10MB</p>
    </div>
  )
}

Calendar & Date Pickers

Calendar

import { Calendar } from "@/components/ui/calendar"
import { useState } from "react"

const [date, setDate] = useState<Date | undefined>(new Date())

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  className="rounded-md border"
/>

Date Picker with Popover

import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Calendar } from "@/components/ui/calendar"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">
      <CalendarIcon className="mr-2 h-4 w-4" />
      {date ? format(date, "PPP") : "Pick a date"}
    </Button>
  </PopoverTrigger>
  <PopoverContent className="w-auto p-0">
    <Calendar
      mode="single"
      selected={date}
      onSelect={setDate}
    />
  </PopoverContent>
</Popover>

Notifications & Toasts

Toast Notification

import { useToast } from "@/components/ui/use-toast"

const { toast } = useToast()

<Button
  onClick={() => {
    toast({
      title: "Success!",
      description: "Your changes have been saved.",
    })
  }}
>
  Show Toast
</Button>

Toast Variants

// Success
toast({
  title: "Success",
  description: "Operation completed successfully",
})

// Error
toast({
  variant: "destructive",
  title: "Error",
  description: "Something went wrong",
})

// With Action
toast({
  title: "Email sent",
  description: "Check your inbox",
  action: <Button size="sm">Undo</Button>,
})

Loading States

Skeleton Loader

import { Skeleton } from "@/components/ui/skeleton"

<div className="space-y-4">
  <Skeleton className="h-12 w-full" />
  <Skeleton className="h-4 w-3/4" />
  <Skeleton className="h-4 w-1/2" />
</div>

Spinner

import { Loader2 } from "lucide-react"

<div className="flex items-center justify-center h-screen">
  <Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>

Progress Bar

import { Progress } from "@/components/ui/progress"

<Progress value={60} className="w-full" />

Responsive Patterns

Mobile Menu Toggle

"use client"

import { Menu, X } from "lucide-react"
import { useState } from "react"

export function MobileNav() {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <>
      <button onClick={() => setIsOpen(!isOpen)} className="md:hidden">
        {isOpen ? <X /> : <Menu />}
      </button>
      
      {isOpen && (
        <div className="md:hidden absolute top-16 left-0 right-0 bg-white border-b p-4">
          <nav className="flex flex-col gap-4">
            <a href="#">Home</a>
            <a href="#">About</a>
            <a href="#">Contact</a>
          </nav>
        </div>
      )}
    </>
  )
}

Responsive Grid

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <Card>Item 1</Card>
  <Card>Item 2</Card>
  <Card>Item 3</Card>
</div>

Best Practices

  1. Always use semantic HTML: Button elements for buttons, not divs
  2. Include ARIA labels: Especially for icon-only buttons
  3. Test keyboard navigation: Tab through all interactive elements
  4. Mobile-first responsive: Start mobile, enhance for desktop
  5. Loading states: Always show feedback during async operations
  6. Error states: Clear error messages with recovery options
  7. Empty states: Show helpful messages when no data
  8. Consistent spacing: Use Tailwind's spacing scale (4, 8, 12, 16, etc.)

Component Library: Shadcn/ui (https://ui.shadcn.com)
Styling: Tailwind CSS (https://tailwindcss.com)
Icons: Lucide React (https://lucide.dev)