article

Shadcn/ui ที่ทำให้ผมเลิกเขียน CSS ตั้งแต่นั้นมา

14 min read

วันที่เหนื่อยกับ CSS

ยังจำได้เลยวันที่ต้องเขียน modal component เพื่อใช้ในโปรเจค เขียน CSS จากศูนย์:

/* modal.css - ไฟล์ที่ทำให้ปวดหัว */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  border-radius: 8px;
  padding: 24px;
  max-width: 500px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  transform: scale(0.9);
  transition: transform 0.2s ease;
}

.modal-content.open {
  transform: scale(1);
}

/* และอีกเป็นร้อยๆ บรรทัด... */

ปัญหาที่เจอ:

  • ใช้เวลานาน เขียน CSS ครั้งเดียว 2-3 ชั่วโมง
  • Responsive ต้องเขียนเพิ่ม
  • Accessibility ลืมใส่
  • Dark mode ต้องเขียนใหม่ทั้งหมด
  • Consistency ระหว่าง components ไม่มี

แล้วก็มาเจอ shadcn/ui ที่เปลี่ยนชีวิต UI development ไปเลย! 🎯

การรู้จัก shadcn/ui ครั้งแรก

ตอนแรกคิดว่า “อีก UI library นึง ที่ต้องมา vendor lock-in” แต่พอดูดีดี เจอว่ามันไม่ใช่ library!

shadcn/ui คือ:

  • Copy & paste components ไม่ใช่ npm package
  • Built on Radix UI (accessibility เจ๋ง)
  • Tailwind CSS styling
  • TypeScript first
  • Customizable ได้ 100%
# Setup ครั้งแรก
npx shadcn-ui@latest init

# ✨ Magic begins!
npx shadcn-ui@latest add button
npx shadcn-ui@latest add modal
npx shadcn-ui@latest add form

ประสบการณ์แรกที่ใช้

Before: Modal component จากนรก

// Modal.jsx - เขียนเอง 200+ lines
import React, { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import './Modal.css';

const Modal = ({ isOpen, onClose, children, title }) => {
  const [isAnimating, setIsAnimating] = useState(false);
  const modalRef = useRef();

  useEffect(() => {
    if (isOpen) {
      setIsAnimating(true);
      document.body.style.overflow = 'hidden';
      
      // Focus management
      const focusableElements = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const firstElement = focusableElements?.[0];
      firstElement?.focus();
      
      // Escape key handler
      const handleEscape = (e) => {
        if (e.key === 'Escape') onClose();
      };
      document.addEventListener('keydown', handleEscape);
      
      return () => {
        document.body.style.overflow = 'unset';
        document.removeEventListener('keydown', handleEscape);
      };
    }
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div 
      className="modal-overlay" 
      onClick={onClose}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <div 
        ref={modalRef}
        className={`modal-content ${isAnimating ? 'open' : ''}`}
        onClick={e => e.stopPropagation()}
      >
        <div className="modal-header">
          <h2 id="modal-title">{title}</h2>
          <button 
            className="modal-close"
            onClick={onClose}
            aria-label="Close modal"
          >
            ×
          </button>
        </div>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>,
    document.body
  );
};

After: shadcn/ui Dialog

// ใช้ shadcn/ui - 10 lines จบ!
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

const MyModal = () => {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>Open Modal</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>My Amazing Modal</DialogTitle>
        </DialogHeader>
        <p>Modal content goes here!</p>
      </DialogContent>
    </Dialog>
  )
}

ได้อะไรฟรีๆ:

  • ✅ Accessibility (ARIA, focus management, keyboard navigation)
  • ✅ Responsive design
  • ✅ Dark mode support
  • ✅ Smooth animations
  • ✅ Portal rendering
  • ✅ TypeScript support

เคสจริง: สร้าง Form ที่ใช้งานได้จริง

ความท้าทาย: Registration Form

// ก่อนใช้ shadcn/ui - Form validation นรก
const RegistrationForm = () => {
  const [errors, setErrors] = useState({});
  const [values, setValues] = useState({
    email: '',
    password: '',
    confirmPassword: ''
  });

  const validate = () => {
    const newErrors = {};
    
    if (!values.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
      newErrors.email = 'Email is invalid';
    }
    
    if (!values.password) {
      newErrors.password = 'Password is required';
    } else if (values.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }
    
    if (values.password !== values.confirmPassword) {
      newErrors.confirmPassword = 'Passwords do not match';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      // Submit logic
    }
  };

  return (
    <form onSubmit={handleSubmit} className="registration-form">
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={values.email}
          onChange={(e) => setValues({...values, email: e.target.value})}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>
      {/* อีกเป็นร้อยบรรทัด... */}
    </form>
  );
};

หลังใช้ shadcn/ui + React Hook Form + Zod

// shadcn/ui + modern form libraries = magic! ✨
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
  email: z.string().email("Invalid email address"),
  password: z.string().min(8, "Password must be at least 8 characters"),
  confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
})

const RegistrationForm = () => {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: "",
      password: "",
      confirmPassword: "",
    },
  })

  const onSubmit = (values) => {
    console.log(values)
  }

  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 placeholder="Enter your 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>
          )}
        />
        
        <FormField
          control={form.control}
          name="confirmPassword"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Confirm Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Register</Button>
      </form>
    </Form>
  )
}

ข้อดีที่ได้:

  • Validation อัตโนมัติ
  • Error messages สวยงาม
  • TypeScript type safety
  • Accessibility built-in
  • Responsive design

ความผิดพลาดที่เจอตอนเริ่มใช้

1. ไม่เข้าใจ Compound Components Pattern

// ❌ คิดว่าใช้แบบ props drilling
<Dialog 
  isOpen={true} 
  title="My Modal" 
  onClose={() => {}}
>
  Content here
</Dialog>

// ✅ จริงๆ ใช้ compound pattern
<Dialog open={isOpen} onOpenChange={setIsOpen}>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>My Modal</DialogTitle>
    </DialogHeader>
    Content here
  </DialogContent>
</Dialog>

2. Customization ที่ผิด

// ❌ พยายาม override ด้วย CSS
.my-custom-button {
  background-color: red !important; /* 😱 */
}

// ✅ ใช้ className variants หรือ CSS variables
<Button className="bg-red-500 hover:bg-red-600">
  Custom Button
</Button>

// หรือสร้าง variant ใหม่
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        danger: "bg-red-500 text-white hover:bg-red-600", // ✨ custom variant
      },
    }
  }
)

3. Dark Mode ที่งง

// เริ่มต้นต้องเซต theme provider
import { ThemeProvider } from "@/components/theme-provider"

function App() {
  return (
    <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
      <Router>
        {/* app content */}
      </Router>
    </ThemeProvider>
  )
}

// Toggle theme
import { useTheme } from "@/components/theme-provider"

const ThemeToggle = () => {
  const { setTheme, theme } = useTheme()
  
  return (
    <Button
      variant="outline"
      size="icon"
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    </Button>
  )
}

Advanced Patterns ที่เรียนรู้

1. Custom Hook สำหรับ Complex Components

// useDataTable.js - จัดการ table state
import { useState, useMemo } from "react"
import {
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table"

export const useDataTable = ({ data, columns }) => {
  const [sorting, setSorting] = useState([])
  const [columnFilters, setColumnFilters] = useState([])
  const [rowSelection, setRowSelection] = useState({})

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      rowSelection,
    },
  })

  return { table }
}

// DataTable.jsx
const DataTable = ({ data, columns }) => {
  const { table } = useDataTable({ data, columns })

  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          {table.getHeaderGroups().map((headerGroup) => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <TableHead key={header.id}>
                  {header.isPlaceholder ? null : (
                    <Button
                      variant="ghost"
                      onClick={header.column.getToggleSortingHandler()}
                    >
                      {flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                      <ArrowUpDown className="ml-2 h-4 w-4" />
                    </Button>
                  )}
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows?.length ? (
            table.getRowModel().rows.map((row) => (
              <TableRow key={row.id}>
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id}>
                    {flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext()
                    )}
                  </TableCell>
                ))}
              </TableRow>
            ))
          ) : (
            <TableRow>
              <TableCell colSpan={columns.length}>
                No results.
              </TableCell>
            </TableRow>
          )}
        </TableBody>
      </Table>
    </div>
  )
}

2. Generic Form Builder

// FormBuilder.jsx - สร้าง form จาก schema
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Checkbox } from "@/components/ui/checkbox"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"

const FormBuilder = ({ schema, onSubmit, defaultValues = {} }) => {
  const form = useForm({
    resolver: zodResolver(schema.validation),
    defaultValues,
  })

  const renderField = (field) => {
    switch (field.type) {
      case 'text':
      case 'email':
      case 'password':
        return (
          <FormField
            key={field.name}
            control={form.control}
            name={field.name}
            render={({ field: fieldProps }) => (
              <FormItem>
                <FormLabel>{field.label}</FormLabel>
                <FormControl>
                  <Input 
                    type={field.type}
                    placeholder={field.placeholder}
                    {...fieldProps} 
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        )
      
      case 'textarea':
        return (
          <FormField
            key={field.name}
            control={form.control}
            name={field.name}
            render={({ field: fieldProps }) => (
              <FormItem>
                <FormLabel>{field.label}</FormLabel>
                <FormControl>
                  <Textarea 
                    placeholder={field.placeholder}
                    {...fieldProps} 
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        )
      
      case 'select':
        return (
          <FormField
            key={field.name}
            control={form.control}
            name={field.name}
            render={({ field: fieldProps }) => (
              <FormItem>
                <FormLabel>{field.label}</FormLabel>
                <Select onValueChange={fieldProps.onChange} defaultValue={fieldProps.value}>
                  <FormControl>
                    <SelectTrigger>
                      <SelectValue placeholder={field.placeholder} />
                    </SelectTrigger>
                  </FormControl>
                  <SelectContent>
                    {field.options.map((option) => (
                      <SelectItem key={option.value} value={option.value}>
                        {option.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <FormMessage />
              </FormItem>
            )}
          />
        )
      
      case 'checkbox':
        return (
          <FormField
            key={field.name}
            control={form.control}
            name={field.name}
            render={({ field: fieldProps }) => (
              <FormItem className="flex flex-row items-start space-x-3 space-y-0">
                <FormControl>
                  <Checkbox
                    checked={fieldProps.value}
                    onCheckedChange={fieldProps.onChange}
                  />
                </FormControl>
                <div className="space-y-1 leading-none">
                  <FormLabel>{field.label}</FormLabel>
                </div>
              </FormItem>
            )}
          />
        )
    }
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        {schema.fields.map(renderField)}
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

// Usage
const userSchema = {
  validation: z.object({
    name: z.string().min(1, "Name is required"),
    email: z.string().email("Invalid email"),
    role: z.string(),
    newsletter: z.boolean().default(false),
  }),
  fields: [
    { name: 'name', type: 'text', label: 'Full Name', placeholder: 'Enter your name' },
    { name: 'email', type: 'email', label: 'Email', placeholder: 'Enter your email' },
    { 
      name: 'role', 
      type: 'select', 
      label: 'Role', 
      placeholder: 'Select a role',
      options: [
        { value: 'admin', label: 'Administrator' },
        { value: 'user', label: 'User' },
        { value: 'moderator', label: 'Moderator' }
      ]
    },
    { name: 'newsletter', type: 'checkbox', label: 'Subscribe to newsletter' },
  ]
}

<FormBuilder 
  schema={userSchema}
  onSubmit={(data) => console.log(data)}
/>

3. Responsive Navigation

// Navigation.jsx - mobile + desktop
import { useState } from "react"
import { Menu, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetContent,
  SheetTrigger,
} from "@/components/ui/sheet"
import { cn } from "@/lib/utils"

const Navigation = () => {
  const [isOpen, setIsOpen] = useState(false)

  const navItems = [
    { href: "/", label: "Home" },
    { href: "/about", label: "About" },
    { href: "/services", label: "Services" },
    { href: "/contact", label: "Contact" },
  ]

  return (
    <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="container flex h-14 items-center">
        {/* Logo */}
        <div className="mr-4 flex">
          <a className="mr-6 flex items-center space-x-2" href="/">
            <span className="font-bold">MyApp</span>
          </a>
        </div>

        {/* Desktop Navigation */}
        <nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
          {navItems.map((item) => (
            <a
              key={item.href}
              href={item.href}
              className="transition-colors hover:text-foreground/80 text-foreground/60"
            >
              {item.label}
            </a>
          ))}
        </nav>

        <div className="flex flex-1 items-center justify-end space-x-2">
          {/* Mobile Navigation */}
          <Sheet open={isOpen} onOpenChange={setIsOpen}>
            <SheetTrigger asChild>
              <Button variant="ghost" size="icon" className="md:hidden">
                <Menu className="h-5 w-5" />
                <span className="sr-only">Toggle menu</span>
              </Button>
            </SheetTrigger>
            <SheetContent side="left">
              <nav className="grid gap-6 text-lg font-medium">
                {navItems.map((item) => (
                  <a
                    key={item.href}
                    href={item.href}
                    onClick={() => setIsOpen(false)}
                    className="hover:text-foreground"
                  >
                    {item.label}
                  </a>
                ))}
              </nav>
            </SheetContent>
          </Sheet>

          {/* Theme Toggle */}
          <ThemeToggle />
          
          {/* User Menu */}
          <Button variant="outline" size="sm">
            Sign In
          </Button>
        </div>
      </div>
    </header>
  )
}

Performance Optimizations

1. Lazy Loading Components

// LazyComponents.jsx
import { lazy, Suspense } from "react"
import { Skeleton } from "@/components/ui/skeleton"

// Lazy load heavy components
const DataTable = lazy(() => import("@/components/data-table"))
const Chart = lazy(() => import("@/components/chart"))

const Dashboard = () => {
  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">Dashboard</h1>
      
      <Suspense 
        fallback={
          <div className="space-y-2">
            <Skeleton className="h-4 w-[250px]" />
            <Skeleton className="h-4 w-[200px]" />
            <Skeleton className="h-[200px] w-full" />
          </div>
        }
      >
        <DataTable />
      </Suspense>
      
      <Suspense fallback={<Skeleton className="h-[300px] w-full" />}>
        <Chart />
      </Suspense>
    </div>
  )
}

2. Virtual Scrolling สำหรับ Large Lists

// VirtualList.jsx
import { FixedSizeList as List } from "react-window"
import { Card, CardContent } from "@/components/ui/card"

const VirtualList = ({ items, height = 400 }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      <Card className="mx-2 mb-2">
        <CardContent className="p-4">
          <h3 className="font-semibold">{items[index].title}</h3>
          <p className="text-sm text-muted-foreground">
            {items[index].description}
          </p>
        </CardContent>
      </Card>
    </div>
  )

  return (
    <List
      height={height}
      itemCount={items.length}
      itemSize={120}
      className="border rounded-md"
    >
      {Row}
    </List>
  )
}

// Usage กับข้อมูลเยอะ
const itemsData = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  title: `Item ${i + 1}`,
  description: `Description for item ${i + 1}`,
}))

<VirtualList items={itemsData} height={500} />

Testing shadcn/ui Components

// Button.test.jsx
import { render, screen, fireEvent } from "@testing-library/react"
import { Button } from "@/components/ui/button"

describe("Button", () => {
  it("renders correctly", () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole("button")).toHaveTextContent("Click me")
  })

  it("handles click events", () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByRole("button"))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it("applies variant classes correctly", () => {
    render(<Button variant="destructive">Delete</Button>)
    const button = screen.getByRole("button")
    expect(button).toHaveClass("bg-destructive")
  })

  it("supports disabled state", () => {
    render(<Button disabled>Disabled</Button>)
    expect(screen.getByRole("button")).toBeDisabled()
  })
})

// Form.test.jsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { RegistrationForm } from "@/components/registration-form"

describe("RegistrationForm", () => {
  it("shows validation errors", async () => {
    const user = userEvent.setup()
    render(<RegistrationForm />)

    const submitButton = screen.getByRole("button", { name: /register/i })
    await user.click(submitButton)

    await waitFor(() => {
      expect(screen.getByText("Invalid email address")).toBeInTheDocument()
    })
  })

  it("submits valid form data", async () => {
    const user = userEvent.setup()
    const mockSubmit = jest.fn()
    render(<RegistrationForm onSubmit={mockSubmit} />)

    await user.type(screen.getByLabelText(/email/i), "test@example.com")
    await user.type(screen.getByLabelText(/^password/i), "password123")
    await user.type(screen.getByLabelText(/confirm password/i), "password123")
    
    await user.click(screen.getByRole("button", { name: /register/i }))

    await waitFor(() => {
      expect(mockSubmit).toHaveBeenCalledWith({
        email: "test@example.com",
        password: "password123",
        confirmPassword: "password123"
      })
    })
  })
})

Storybook Integration

// Button.stories.jsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from '@/components/ui/button'

const meta: Meta<typeof Button> = {
  title: 'UI/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
    },
    size: {
      control: { type: 'select' },
      options: ['default', 'sm', 'lg', 'icon'],
    },
  },
}

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
  args: {
    children: 'Button',
  },
}

export const Destructive: Story = {
  args: {
    variant: 'destructive',
    children: 'Delete',
  },
}

export const WithIcon: Story = {
  args: {
    children: (
      <>
        <Mail className="mr-2 h-4 w-4" />
        Send Email
      </>
    ),
  },
}

export const Loading: Story = {
  args: {
    disabled: true,
    children: (
      <>
        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
        Please wait
      </>
    ),
  },
}

Production Tips

1. Bundle Size Optimization

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['react', 'react-dom'], // ถ้า SSR
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          ui: ['@radix-ui/react-dialog', '@radix-ui/react-form'],
        }
      }
    }
  }
})

// แยก components ที่ไม่ค่อยใช้
const HeavyComponent = lazy(() => import('./heavy-component'))

2. Accessibility Audit

# Install axe-core
npm install --save-dev @axe-core/react

# App.jsx
if (process.env.NODE_ENV !== 'production') {
  import('@axe-core/react').then((axe) => {
    axe.default(React, ReactDOM, 1000)
  })
}

3. Theme Customization

/* globals.css */
@layer base {
  :root {
    /* Custom color palette */
    --primary: 220 14% 11%;
    --primary-foreground: 210 20% 98%;
    
    /* Brand colors */
    --brand-primary: 210 100% 50%;
    --brand-secondary: 280 100% 70%;
    
    /* Custom shadows */
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
  }
  
  .dark {
    --primary: 210 20% 98%;
    --primary-foreground: 220 14% 11%;
  }
}

/* Component-specific customizations */
.btn-brand {
  @apply bg-brand-primary hover:bg-brand-primary/90;
}

สรุป: shadcn/ui ที่เปลี่ยนชีวิต

ก่อนใช้ shadcn/ui:

  • เขียน CSS ใช้เวลานาน
  • Accessibility ลืมใส่
  • Consistency ไม่มี
  • Dark mode เขียนใหม่ทั้งหมด

หลังใช้ shadcn/ui:

  • Copy & paste แล้วใช้ได้เลย
  • Accessibility ครบ
  • Design system สวยงาม
  • Customizable 100%

ข้อดีที่ได้จริง:

  • Development speed เร็วขึ้น 70%
  • Code quality ดีขึ้น (TypeScript + accessibility)
  • Maintenance ง่ายขึ้น
  • Team consistency มี design language เดียวกัน

ข้อเสียที่ต้องยอมรับ:

  • Learning curve ต้องเรียนรู้ Radix UI patterns
  • Bundle size ใหญ่กว่า vanilla CSS
  • Dependency ต้องพึ่ง Tailwind CSS

คำแนะนำ:

  1. เริ่มจาก basic components ก่อน (Button, Input)
  2. เรียนรู้ compound patterns ของ Radix UI
  3. ใช้ TypeScript ให้เต็มที่
  4. Setup Storybook สำหรับ documentation
  5. Customize ให้เข้ากับ brand

shadcn/ui มันเหมือน Swiss Army Knife ของ UI development

มีเครื่องมือครบครัน ใช้งานง่าย แต่ทรงพลังมาก 🔧✨

ตอนนี้ไม่อยากกลับไปเขียน CSS จากศูนย์อีกแล้ว เพราะ ชีวิตมันง่ายขึ้นเยอะ! 😄