shadcn/ui คืออะไร
shadcn/ui เป็น collection ของ reusable UI components สำหรับ React ที่สร้างบน Radix UI primitives และ styled ด้วย Tailwind CSS ออกแบบโดย shadcn จุดเด่นคือไม่ได้เป็น npm package ที่ install แบบ dependency แต่เป็น components ที่ copy เข้า project โดยตรง ทำให้ customize ได้เต็มที่
หลักการของ shadcn/ui ได้แก่ Ownership คุณเป็นเจ้าของ code ไม่ต้องพึ่ง library updates, Composability components ออกแบบให้ compose ร่วมกันได้, Accessibility ใช้ Radix UI ที่ accessible by default, Customizable แก้ไข styles ได้ตามต้องการ, TypeScript support ครบถ้วน, Dark mode built-in support
Open Source Contribution การ contribute ให้ shadcn/ui หรือ open source projects ที่คล้ายกัน ช่วยพัฒนาทักษะ coding เรียนรู้ best practices จาก maintainers สร้าง portfolio และช่วย community ในบทความนี้จะครอบคลุมตั้งแต่การใช้งาน shadcn/ui ไปจนถึงวิธี contribute code กลับไปยัง open source
ติดตั้งและเริ่มใช้งาน shadcn/ui
Setup shadcn/ui ใน Next.js project
# === shadcn/ui Installation ===
# 1. Create Next.js Project
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
# 2. Initialize shadcn/ui
npx shadcn@latest init
# Configuration prompts:
# Style: Default
# Base color: Slate
# CSS variables: Yes
# Tailwind config: tailwind.config.ts
# Components: @/components
# Utils: @/lib/utils
# 3. Add Components
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add input
npx shadcn@latest add label
npx shadcn@latest add select
npx shadcn@latest add table
npx shadcn@latest add tabs
npx shadcn@latest add toast
# 4. Add Multiple Components at Once
npx shadcn@latest add button card dialog input label
# 5. Project Structure After Setup
# my-app/
# ├── components/
# │ └── ui/
# │ ├── button.tsx
# │ ├── card.tsx
# │ ├── dialog.tsx
# │ ├── input.tsx
# │ └── ...
# ├── lib/
# │ └── utils.ts # cn() utility function
# ├── app/
# │ ├── globals.css # CSS variables for theming
# │ └── layout.tsx
# ├── components.json # shadcn/ui configuration
# └── tailwind.config.ts
# 6. Verify Installation
cat components.json
# Should show paths, aliases, and style configuration
# 7. Check utils.ts
cat lib/utils.ts
# import { type ClassValue, clsx } from "clsx"
# import { twMerge } from "tailwind-merge"
# export function cn(...inputs: ClassValue[]) {
# return twMerge(clsx(inputs))
# }
echo "shadcn/ui installed successfully"
สร้าง Custom Components
สร้าง custom components บน shadcn/ui
// === Custom Components with shadcn/ui ===
// 1. Custom DataTable Component
// components/ui/data-table.tsx
// ===================================
import * as React from "react";
// Types
interface Column {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
sortable?: boolean;
}
interface DataTableProps {
data: T[];
columns: Column[];
searchable?: boolean;
pageSize?: number;
}
function DataTable>({
data, columns, searchable = true, pageSize = 10
}: DataTableProps) {
const [search, setSearch] = React.useState("");
const [sortKey, setSortKey] = React.useState(null);
const [sortDir, setSortDir] = React.useState<"asc" | "desc">("asc");
const [page, setPage] = React.useState(0);
// Filter
const filtered = React.useMemo(() => {
if (!search) return data;
return data.filter(row =>
columns.some(col =>
String(row[col.key]).toLowerCase().includes(search.toLowerCase())
)
);
}, [data, columns, search]);
// Sort
const sorted = React.useMemo(() => {
if (!sortKey) return filtered;
return [...filtered].sort((a, b) => {
const aVal = a[sortKey];
const bVal = b[sortKey];
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return sortDir === "asc" ? cmp : -cmp;
});
}, [filtered, sortKey, sortDir]);
// Paginate
const paged = sorted.slice(page * pageSize, (page + 1) * pageSize);
const totalPages = Math.ceil(sorted.length / pageSize);
const handleSort = (key: keyof T) => {
if (sortKey === key) {
setSortDir(d => d === "asc" ? "desc" : "asc");
} else {
setSortKey(key);
setSortDir("asc");
}
};
return { paged, totalPages, page, handleSort, search, setSearch, setPage };
}
// 2. Custom StatusBadge Component
// components/ui/status-badge.tsx
// ===================================
type BadgeVariant = "success" | "warning" | "error" | "info" | "default";
interface StatusBadgeProps {
status: string;
variant?: BadgeVariant;
}
const variantStyles: Record = {
success: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
error: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
info: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
default: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300",
};
function getVariantFromStatus(status: string): BadgeVariant {
const map: Record = {
active: "success", running: "success", healthy: "success",
pending: "warning", warning: "warning", degraded: "warning",
error: "error", failed: "error", critical: "error",
info: "info", new: "info",
};
return map[status.toLowerCase()] || "default";
}
// 3. Custom MetricCard Component
// components/ui/metric-card.tsx
// ===================================
interface MetricCardProps {
title: string;
value: string | number;
change?: number;
icon?: React.ReactNode;
description?: string;
}
// Usage example:
//
console.log("Custom components defined");
console.log("DataTable, StatusBadge, MetricCard ready");
วิธี Contribute ให้ Open Source
ขั้นตอน contribute ให้ shadcn/ui และ open source projects
# === Open Source Contribution Guide ===
# 1. Fork and Clone Repository
# ===================================
# Go to https://github.com/shadcn-ui/ui
# Click "Fork" button
git clone https://github.com/YOUR-USERNAME/ui.git
cd ui
git remote add upstream https://github.com/shadcn-ui/ui.git
# 2. Setup Development Environment
pnpm install
pnpm dev
# 3. Create Feature Branch
git checkout -b feat/new-component-timeline
# Naming conventions:
# feat/description — new feature
# fix/description — bug fix
# docs/description — documentation
# refactor/description — code refactoring
# 4. Find Issues to Work On
# ===================================
# Look for labels:
# - "good first issue" — beginner friendly
# - "help wanted" — maintainers want help
# - "bug" — confirmed bugs
# - "enhancement" — feature requests
#
# Before starting:
# 1. Comment on the issue saying you want to work on it
# 2. Wait for maintainer to assign you
# 3. Ask questions if anything is unclear
# 4. Read CONTRIBUTING.md carefully
# 5. Contribution Checklist
# ===================================
# [ ] Read CONTRIBUTING.md and CODE_OF_CONDUCT.md
# [ ] Issue exists and is assigned to you
# [ ] Branch created from latest main
# [ ] Code follows project style guide
# [ ] TypeScript types are correct
# [ ] Components are accessible (keyboard, screen reader)
# [ ] Dark mode works correctly
# [ ] Tests added/updated
# [ ] Documentation updated
# [ ] No console errors or warnings
# [ ] PR description explains the change
# 6. Commit Message Convention
# ===================================
# Format: type(scope): description
#
# Types:
# feat: new feature
# fix: bug fix
# docs: documentation
# style: formatting, missing semicolons
# refactor: code restructuring
# test: adding tests
# chore: maintenance tasks
#
# Examples:
git commit -m "feat(timeline): add Timeline component"
git commit -m "fix(button): fix focus ring on dark mode"
git commit -m "docs(card): add usage examples"
# 7. Create Pull Request
git push origin feat/new-component-timeline
# Go to GitHub and create PR
# Fill in PR template:
# - What does this PR do?
# - Screenshots (if UI change)
# - How to test
# - Related issues (#123)
# 8. Code Review Process
# ===================================
# - Maintainer reviews your code
# - Address feedback promptly
# - Be open to suggestions
# - Don't take feedback personally
# - Push fixes to the same branch (auto-updates PR)
#
# Common feedback:
# - "Can you add tests for this?"
# - "This doesn't match our style guide"
# - "Can you use the existing utility function?"
# - "This needs accessibility improvements"
echo "Contribution guide complete"
Testing และ Quality Assurance
เขียน tests สำหรับ components
#!/usr/bin/env python3
# component_qa.py — Component Quality Assurance
import json
import logging
from typing import Dict, List
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("qa")
class ComponentQA:
def __init__(self):
self.checks = []
def accessibility_checklist(self, component_name):
"""Generate accessibility checklist for a component"""
return {
"component": component_name,
"checks": {
"keyboard_navigation": [
"Tab focuses the component",
"Enter/Space activates the component",
"Escape closes dialogs/dropdowns",
"Arrow keys navigate within component",
"Focus is visible (focus ring)",
],
"screen_reader": [
"Has appropriate ARIA role",
"Has accessible label (aria-label or aria-labelledby)",
"State changes announced (aria-expanded, aria-selected)",
"Error messages linked (aria-describedby)",
"Live regions for dynamic content (aria-live)",
],
"visual": [
"Color contrast ratio >= 4.5:1 (text)",
"Color contrast ratio >= 3:1 (large text, UI)",
"Not relying on color alone for information",
"Works at 200% zoom",
"Respects prefers-reduced-motion",
],
"semantic": [
"Uses correct HTML elements (button, not div)",
"Headings are hierarchical",
"Lists use ul/ol/li",
"Form inputs have labels",
"Images have alt text",
],
},
}
def test_plan(self, component_name):
"""Generate test plan for a shadcn/ui component"""
return {
"component": component_name,
"unit_tests": [
f"{component_name} renders without crashing",
f"{component_name} renders children correctly",
f"{component_name} applies className prop",
f"{component_name} forwards ref correctly",
f"{component_name} handles disabled state",
f"{component_name} handles loading state",
],
"interaction_tests": [
f"{component_name} responds to click events",
f"{component_name} responds to keyboard events",
f"{component_name} manages focus correctly",
f"{component_name} handles hover state",
],
"visual_tests": [
f"{component_name} matches snapshot (light mode)",
f"{component_name} matches snapshot (dark mode)",
f"{component_name} renders all variants correctly",
f"{component_name} is responsive",
],
"accessibility_tests": [
f"{component_name} has no axe violations",
f"{component_name} is keyboard accessible",
f"{component_name} has correct ARIA attributes",
],
}
def pr_review_checklist(self):
"""Checklist for reviewing PRs"""
return {
"code_quality": [
"TypeScript types are strict (no any)",
"No unused imports or variables",
"Follows project naming conventions",
"No hardcoded values (use CSS variables)",
"Error handling is proper",
],
"component_design": [
"Props interface is well-defined",
"Component is composable",
"Supports className override via cn()",
"Forwards ref with React.forwardRef",
"Has sensible default props",
],
"testing": [
"Unit tests cover main functionality",
"Edge cases are tested",
"Accessibility tests included",
"No flaky tests",
],
"documentation": [
"Component has JSDoc comments",
"Usage examples provided",
"Props are documented",
"Breaking changes noted",
],
}
qa = ComponentQA()
a11y = qa.accessibility_checklist("Button")
print("Accessibility:", json.dumps(a11y["checks"]["keyboard_navigation"], indent=2))
tests = qa.test_plan("Dialog")
print("\nTest Plan:", json.dumps(tests["unit_tests"], indent=2))
review = qa.pr_review_checklist()
print("\nPR Review:", json.dumps(review["code_quality"], indent=2))
Best Practices สำหรับ Component Library
แนวทางสร้าง component library ที่ดี
# === Component Library Best Practices ===
# 1. Component API Design
# ===================================
# Good: Composable pattern
#
#
# Title
# Description
#
# Content here
#
#
#
#
# Bad: Props-heavy monolithic component
#
# 2. Variant System with cva
# ===================================
# import { cva, type VariantProps } from "class-variance-authority"
#
# 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",
# outline: "border border-input bg-background hover:bg-accent",
# secondary: "bg-secondary text-secondary-foreground",
# ghost: "hover:bg-accent hover:text-accent-foreground",
# link: "text-primary underline-offset-4 hover:underline",
# },
# size: {
# default: "h-10 px-4 py-2",
# sm: "h-9 rounded-md px-3",
# lg: "h-11 rounded-md px-8",
# icon: "h-10 w-10",
# },
# },
# defaultVariants: {
# variant: "default",
# size: "default",
# },
# }
# )
# 3. Theming with CSS Variables
# ===================================
# globals.css:
# :root {
# --background: 0 0% 100%;
# --foreground: 222.2 84% 4.9%;
# --primary: 222.2 47.4% 11.2%;
# --primary-foreground: 210 40% 98%;
# --secondary: 210 40% 96.1%;
# --muted: 210 40% 96.1%;
# --accent: 210 40% 96.1%;
# --destructive: 0 84.2% 60.2%;
# --border: 214.3 31.8% 91.4%;
# --ring: 222.2 84% 4.9%;
# --radius: 0.5rem;
# }
#
# .dark {
# --background: 222.2 84% 4.9%;
# --foreground: 210 40% 98%;
# --primary: 210 40% 98%;
# ...
# }
# 4. File Organization
# ===================================
# components/
# ├── ui/ # shadcn/ui base components
# │ ├── button.tsx
# │ ├── card.tsx
# │ └── dialog.tsx
# ├── shared/ # Custom shared components
# │ ├── data-table.tsx
# │ ├── metric-card.tsx
# │ └── status-badge.tsx
# ├── features/ # Feature-specific components
# │ ├── dashboard/
# │ ├── auth/
# │ └── settings/
# └── layouts/ # Layout components
# ├── header.tsx
# ├── sidebar.tsx
# └── footer.tsx
# 5. Performance Tips
# ===================================
# - Use React.lazy() for heavy components
# - Memoize expensive renders with React.memo
# - Use CSS animations instead of JS animations
# - Lazy load icons (dynamic import)
# - Tree-shake unused components
# - Use Radix UI's asChild pattern for flexibility
echo "Best practices documented"
FAQ คำถามที่พบบ่อย
Q: shadcn/ui กับ Material UI ต่างกันอย่างไร?
A: shadcn/ui copy components เข้า project (ownership) ใช้ Tailwind CSS สำหรับ styling, components เป็นของคุณ customize ได้เต็มที่ ไม่มี runtime overhead จาก library, bundle size เล็ก Material UI เป็น npm package (dependency) ใช้ Emotion/styled-components สำหรับ styling, ต้องพึ่ง library updates, มี runtime CSS-in-JS overhead, bundle size ใหญ่กว่า แนะนำ shadcn/ui สำหรับ projects ที่ต้องการ customization สูงและ performance ดี Material UI สำหรับ projects ที่ต้องการ components สำเร็จรูปครบ ไม่ต้อง customize มาก
Q: จะเริ่ม contribute open source อย่างไร?
A: เริ่มจาก projects ที่ใช้อยู่แล้ว อ่าน CONTRIBUTING.md และ CODE_OF_CONDUCT.md หา issues ที่มี label good first issue เริ่มจาก documentation fixes, typo corrections, test additions ก่อนแล้วค่อยทำ feature ใหญ่ขึ้น comment บน issue ก่อนเริ่มทำ ใช้ conventional commits อย่ากลัว code review ถือเป็นโอกาสเรียนรู้ สำหรับ shadcn/ui เริ่มจากสร้าง component ใหม่ที่ community ร้องขอใน GitHub Discussions
Q: cn() utility function ทำอะไร?
A: cn() เป็น utility function ที่รวม clsx กับ tailwind-merge clsx ช่วย conditionally join classNames เช่น cn("px-4", isActive && "bg-blue-500") tailwind-merge ช่วย resolve conflicting Tailwind classes เช่น cn("px-4 px-6") จะได้ "px-6" (ไม่ซ้ำกัน) ทำให้ components รับ className prop แล้ว merge กับ default styles ได้อย่างถูกต้อง ใช้ทุกที่ใน shadcn/ui components
Q: shadcn/ui รองรับ React Server Components ไหม?
A: shadcn/ui components ส่วนใหญ่เป็น Client Components (ใช้ use client) เพราะมี interactivity เช่น Dialog, Dropdown, Tabs แต่ components ที่ไม่มี interactivity เช่น Card, Badge, Separator ใช้เป็น Server Components ได้ ใน Next.js App Router ให้ import client components ใน page.tsx ที่เป็น Server Component ได้ปกติ เพราะ Next.js จัดการ boundary ให้อัตโนมัติ สำหรับ performance ดีที่สุด แยก static content เป็น Server Components และ interactive parts เป็น Client Components
