Shadcn UI คืออะไร
Shadcn UI เป็น Component Library แนวใหม่ที่ไม่ได้ติดตั้งเป็น npm package แต่ Copy Source Code ลงในโปรเจคโดยตรง ทำให้ Developer มี Full Control สามารถปรับแต่ง Component ได้ตามต้องการ สร้างบน Radix UI Primitives (Accessible) และ Tailwind CSS (Styling)
การใช้ Automation Scripts ร่วมกับ Shadcn UI ช่วยเพิ่มประสิทธิภาพในการ Setup Project, Generate Components หลายตัวพร้อมกัน, Customize Theme อัตโนมัติ และสร้าง Documentation จาก Components
Automation Script — Setup และ Generate Components
#!/bin/bash
# shadcn-setup.sh — Automation Script สำหรับ Shadcn UI Setup
# ใช้กับ Next.js + TypeScript + Tailwind CSS
set -e
PROJECT_NAME=
echo "=== Setting up Shadcn UI Project: $PROJECT_NAME ==="
# 1. สร้าง Next.js Project
npx create-next-app@latest "$PROJECT_NAME" \
--typescript --tailwind --eslint --app \
--src-dir --import-alias "@/*" --no-git
cd "$PROJECT_NAME"
# 2. Initialize Shadcn UI
npx shadcn-ui@latest init -y \
--style default \
--base-color slate \
--css-variables
# 3. ติดตั้ง Essential Components
COMPONENTS=(
"button"
"input"
"label"
"card"
"dialog"
"dropdown-menu"
"table"
"tabs"
"toast"
"form"
"select"
"badge"
"alert"
"avatar"
"separator"
"skeleton"
"sheet"
"command"
"popover"
"tooltip"
)
echo "Installing components..."
for comp in ""; do
echo " Adding: $comp"
npx shadcn-ui@latest add "$comp" -y 2>/dev/null || echo " Skipped: $comp"
done
# 4. ติดตั้ง Dependencies เพิ่มเติม
npm install lucide-react @tanstack/react-table date-fns zod \
@hookform/resolvers react-hook-form next-themes
# 5. สร้าง Theme Provider
mkdir -p src/components
cat > src/components/theme-provider.tsx << 'THEME_EOF'
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return {children}
}
THEME_EOF
# 6. สร้าง Toaster Component
cat > src/components/toaster.tsx << 'TOAST_EOF'
"use client"
import { Toaster as SonnerToaster } from "sonner"
export function Toaster() {
return
}
TOAST_EOF
echo ""
echo "=== Setup Complete ==="
echo "Components installed: "
echo "Run: cd $PROJECT_NAME && npm run dev"
Python Script — Component Generator
# shadcn_generator.py — Generate Custom Components จาก Template
import os
import json
import subprocess
from pathlib import Path
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ComponentSpec:
name: str
description: str
props: dict
shadcn_deps: List[str]
template: str = "default"
class ShadcnComponentGenerator:
"""Generate Custom Components บน Shadcn UI"""
def __init__(self, project_path):
self.project_path = Path(project_path)
self.components_dir = self.project_path / "src" / "components"
self.ui_dir = self.components_dir / "ui"
def ensure_shadcn_deps(self, deps: List[str]):
"""ติดตั้ง Shadcn Components ที่จำเป็น"""
for dep in deps:
dep_file = self.ui_dir / f"{dep}.tsx"
if not dep_file.exists():
print(f" Installing shadcn component: {dep}")
subprocess.run(
["npx", "shadcn-ui@latest", "add", dep, "-y"],
cwd=str(self.project_path),
capture_output=True,
)
def generate_data_table(self, name="data-table", columns=None):
"""Generate Data Table Component"""
self.ensure_shadcn_deps(["table", "button", "input",
"dropdown-menu", "badge"])
component_code = '''
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Table, TableBody, TableCell, TableHead,
TableHeader, TableRow,
} from "@/components/ui/table"
import {
DropdownMenu, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
interface DataTableProps {
columns: ColumnDef[]
data: TData[]
searchKey?: string
searchPlaceholder?: string
}
export function DataTable({
columns, data, searchKey, searchPlaceholder = "Search...",
}: DataTableProps) {
const [sorting, setSorting] = React.useState([])
const [columnFilters, setColumnFilters] =
React.useState([])
const [columnVisibility, setColumnVisibility] =
React.useState({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data, columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: { sorting, columnFilters, columnVisibility, rowSelection },
})
return (
{searchKey && (
table.getColumn(searchKey)
?.setFilterValue(e.target.value)}
className="max-w-sm"
/>
)}
{table.getAllColumns()
.filter((col) => col.getCanHide())
.map((col) => (
col.toggleVisibility(!!v)}
>
{col.id}
))}
{table.getHeaderGroups().map((hg) => (
{hg.headers.map((h) => (
{h.isPlaceholder ? null :
flexRender(h.column.columnDef.header, h.getContext())}
))}
))}
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
))}
))
) : (
No results.
)}
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
)
}
'''
output_path = self.components_dir / f"{name}.tsx"
output_path.write_text(component_code.strip(), encoding="utf-8")
print(f"Generated: {output_path}")
def generate_component_index(self):
"""สร้าง Index File สำหรับ Export ทุก Components"""
ui_files = sorted(self.ui_dir.glob("*.tsx"))
exports = []
for f in ui_files:
name = f.stem
exports.append(f'export * from "./ui/{name}"')
index_path = self.components_dir / "index.ts"
index_path.write_text("\n".join(exports), encoding="utf-8")
print(f"Generated index: {index_path} ({len(exports)} exports)")
def audit_accessibility(self):
"""ตรวจสอบ Accessibility ของ Components"""
issues = []
for f in self.ui_dir.glob("*.tsx"):
content = f.read_text(encoding="utf-8", errors="ignore")
# Check for aria labels
if "
CI/CD Script สำหรับ Component Testing
# === GitHub Actions — Shadcn UI CI/CD ===
# .github/workflows/ui-test.yml
name: UI Component Tests
on:
push:
branches: [main]
paths: ["src/components/**"]
pull_request:
branches: [main]
paths: ["src/components/**"]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
# Lint
- name: ESLint
run: npx eslint src/components/ --ext .tsx,.ts
# Type Check
- name: TypeScript
run: npx tsc --noEmit
# Unit Tests
- name: Jest
run: npx jest --coverage --testPathPattern=components
# Accessibility
- name: Axe Accessibility
run: |
npx playwright install chromium
npx playwright test --project=accessibility
# Storybook Build (ถ้ามี)
- name: Build Storybook
run: npx storybook build --quiet
if: hashFiles('**/.storybook') != ''
# Visual Regression (ถ้ามี)
- name: Visual Tests
run: npx playwright test --project=visual
if: hashFiles('**/visual.spec.ts') != ''
# === package.json scripts ===
# {
# "scripts": {
# "shadcn:add": "npx shadcn-ui@latest add",
# "shadcn:diff": "npx shadcn-ui@latest diff",
# "shadcn:audit": "python3 scripts/shadcn_generator.py audit",
# "test:ui": "jest --testPathPattern=components",
# "test:a11y": "playwright test --project=accessibility",
# "lint:ui": "eslint src/components/ --ext .tsx,.ts"
# }
# }
# === Playwright Accessibility Test ===
# tests/accessibility.spec.ts
# import { test, expect } from '@playwright/test'
# import AxeBuilder from '@axe-core/playwright'
#
# test.describe('Component Accessibility', () => {
# test('Button should be accessible', async ({ page }) => {
# await page.goto('/storybook/button')
# const results = await new AxeBuilder({ page }).analyze()
# expect(results.violations).toEqual([])
# })
#
# test('Dialog should be accessible', async ({ page }) => {
# await page.goto('/storybook/dialog')
# const results = await new AxeBuilder({ page }).analyze()
# expect(results.violations).toEqual([])
# })
# })
Best Practices
- ใช้ CLI เท่านั้น: เพิ่ม Components ด้วย npx shadcn-ui add ไม่ Copy จาก GitHub โดยตรง
- Customize ใน UI folder: แก้ไข Components ใน src/components/ui/ ได้เลย เป็น Source Code ของคุณ
- Theme ผ่าน CSS Variables: ใช้ CSS Variables ใน globals.css ปรับ Theme ไม่ต้องแก้ Component
- Accessibility First: Shadcn สร้างบน Radix UI ที่ Accessible อยู่แล้ว อย่าลบ aria attributes
- Auto Import: สร้าง barrel exports ใน index.ts สำหรับ Import ที่สะดวก
- Version Control: Commit Components ลง Git เพราะเป็น Source Code ไม่ใช่ node_modules
Shadcn UI คืออะไร
Component Library สำหรับ React ที่ Copy Source Code ลงโปรเจคโดยตรง ปรับแต่งได้เต็มที่ สร้างบน Radix UI Primitives และ Tailwind CSS มี CLI สำหรับเพิ่ม Components อัตโนมัติ
ทำไมต้องใช้ Automation Script กับ Shadcn UI
Setup เร็วขึ้น Generate Components หลายตัวพร้อมกัน Customize Theme อัตโนมัติ สร้าง Documentation อัตโนมัติ Accessibility Audit และ Integration Testing ลดเวลาจากหลายชั่วโมงเหลือไม่กี่นาที
วิธีติดตั้ง Shadcn UI ทำอย่างไร
npx shadcn-ui@latest init ตอบ Configuration เช่น Style Base Color CSS Variables เพิ่ม Components ด้วย npx shadcn-ui@latest add button dialog table Components ถูก Copy ลง src/components/ui/
Shadcn UI รองรับ Framework อะไรบ้าง
Next.js, Remix, Vite, Astro, Laravel, Gatsby ใช้กับ React และ Tailwind CSS มี Port สำหรับ Vue (shadcn-vue) Svelte (shadcn-svelte) และ Angular ด้วย
สรุป
Shadcn UI เป็น Component Library ที่ให้ Full Control แก่ Developer Automation Scripts ช่วยเพิ่มประสิทธิภาพในการ Setup, Generate Components, Audit Accessibility และ Testing ใช้ CI/CD Pipeline ทดสอบ Components อัตโนมัติทุก PR สิ่งสำคัญคือ Customize ได้เต็มที่เพราะเป็น Source Code ของคุณเอง
