1. Component Libraries คืออะไร — Shadcn vs MUI vs Chakra
Component Library คือชุด UI Components สำเร็จรูปที่นักพัฒนาสามารถนำมาใช้ในโปรเจกต์ได้ทันที เช่น ปุ่ม (Button), การ์ด (Card), Modal, Input, Table และอีกมากมาย แทนที่จะเขียน CSS และ HTML ทุกอย่างจากศูนย์ เราสามารถ import Component เหล่านี้มาใช้ได้เลย ซึ่งช่วยประหยัดเวลา รักษา Consistency ของ UI และมั่นใจได้ว่า Component ผ่านการทดสอบ Accessibility แล้ว
Library ยอดนิยมในระบบนิเวศ React มีหลายตัว แต่ละตัวมีแนวทางที่แตกต่างกัน:
- Material UI (MUI) — ใช้หลักการออกแบบของ Google Material Design มี Component ครบครัน Customize ได้ผ่าน Theme แต่ Bundle size ค่อนข้างใหญ่ เหมาะสำหรับโปรเจกต์ขนาดใหญ่ที่ต้องการความสม่ำเสมอของ Design System
-
Chakra UI — API ที่ใช้งานง่าย มี Style Props (เช่น
p={4}แทน padding) รองรับ Dark Mode ออกนอกกล่อง มี Accessibility ดีมาก แต่ต้องเรียนรู้ API เฉพาะของมัน - Shadcn/ui — แนวคิดใหม่ที่ไม่ใช่ Library แบบปกติ แต่เป็นการ copy โค้ด Component เข้ามาในโปรเจกต์โดยตรง ทำให้ Customize ได้ 100% ใช้ Tailwind CSS สำหรับ Styling และ Radix UI สำหรับ Behavior ไม่มี Dependency ล็อกในเวอร์ชันของ Library
แนวทางการ Styling ของแต่ละ Library ────────────────────────────────────────────────── MUI → Emotion (CSS-in-JS) + MUI Theme Chakra UI → Emotion + Style Props + Chakra Theme Shadcn/ui → Tailwind CSS + CSS Variables ────────────────────────────────────────────────── การติดตั้ง Component ────────────────────────────────────────────────── MUI → npm install @mui/material (ได้ทุก Component) Chakra UI → npm install @chakra-ui/react (ได้ทุก Component) Shadcn/ui → npx shadcn@latest add button (copy เฉพาะที่ต้องการ) ──────────────────────────────────────────────────
Shadcn/ui เป็นเพียงเครื่องมือสำหรับ copy โค้ด Component เข้าโปรเจกต์ ไม่ใช่ npm package ดังนั้นถ้า Library ออก Breaking Change ในเวอร์ชันใหม่ โปรเจกต์ของเราก็ไม่ได้รับผลกระทบ และสามารถแก้ไข Component ได้ตามต้องการโดยไม่ต้องรอ Maintainer ของ Library
2. Tailwind CSS — Utility-First CSS Framework
Tailwind CSS เป็น CSS Framework แนวทาง Utility-First
ซึ่งหมายความว่าแทนที่จะเขียน CSS Class ที่มีความหมายเช่น .card หรือ .btn-primary
เราใช้ Class เล็กๆ ที่ทำสิ่งเดียวโดยตรง เช่น p-4 (padding 1rem),
text-blue-500 (สีน้ำเงิน), flex (display: flex),
rounded-lg (border-radius ขนาดใหญ่)
ข้อดีหลักของ Tailwind:
- ไม่ต้องตั้งชื่อ CSS Class — ปัญหาคลาสสิคในการพัฒนาเว็บคือการตั้งชื่อ Class ด้วย Tailwind เราไม่ต้องคิดว่าจะตั้งชื่อว่าอะไร เพราะใช้ Utility Class ของ Tailwind ได้เลย
- ขนาดไฟล์ CSS เล็ก — Tailwind ใช้เทคโนโลยี Purge/Content Scanning ลบ Class ที่ไม่ได้ใช้ออก ทำให้ CSS ที่ Deploy จริงมีขนาดเล็กมาก
-
Responsive Design ง่าย — ใช้ Prefix เช่น
md:,lg:เช่นgrid-cols-1 md:grid-cols-2 lg:grid-cols-3แสดง 1 คอลัมน์บนมือถือ 2 คอลัมน์บน tablet 3 คอลัมน์บน Desktop -
Dark Mode — เพิ่ม
dark:prefix เช่นbg-white dark:bg-gray-900
Responsive Breakpoints ของ Tailwind ───────────────────────────────────────── (ไม่มี prefix) → mobile first (base) sm: → min-width: 640px md: → min-width: 768px lg: → min-width: 1024px xl: → min-width: 1280px 2xl: → min-width: 1536px ตัวอย่าง: <div class="text-sm md:text-base lg:text-lg"> → มือถือ: text-sm | tablet: text-base | Desktop: text-lg
ใน React ใช้ className แทน class เหมือนปกติ
และสามารถใช้ Library เช่น clsx หรือ cn() จาก Shadcn
เพื่อจัดการ Class แบบเงื่อนไขได้สะดวก เช่น cn('base-class', isActive && 'active-class')
3. Shadcn/ui Setup กับ Vite + React
การติดตั้ง Shadcn/ui ต้องทำหลายขั้นตอนเพราะมันไม่ใช่ npm package ธรรมดา แต่ละขั้นตอนสำคัญและต้องทำตามลำดับ:
-
ติดตั้ง Tailwind CSS — ต้องติดตั้ง
tailwindcss,postcssและautoprefixerเป็น dev dependencies แล้วรันnpx tailwindcss init -pเพื่อสร้างไฟล์ config -
ตั้งค่า tailwind.config.js — กำหนด
contentให้ Tailwind สแกนไฟล์ JSX/TSX เพื่อหา Class ที่ใช้จริง -
เพิ่ม Tailwind directives ใน CSS — เพิ่ม
@tailwind base;,@tailwind components;,@tailwind utilities;ในsrc/index.css -
รัน Shadcn init —
npx shadcn@latest initจะถามตัวเลือกต่างๆ เช่น Style (Default/New York), Color scheme, และ CSS variables -
เพิ่ม Component ทีละตัว —
npx shadcn@latest add buttonจะ copy โค้ด Component มาไว้ที่src/components/ui/button.jsx
Shadcn ใช้ @/ เป็น Path Alias สำหรับ src/
ต้องตั้งค่าใน vite.config.js โดยเพิ่ม
resolve: { alias: { '@': path.resolve(__dirname, './src') } }
ถ้าไม่ตั้งค่าจะเกิด error "Cannot find module '@/components/ui/button'"
4. Components ที่ใช้บ่อย: Button, Card, Input, Dialog, Badge, Table
Shadcn/ui มี Component พร้อมใช้หลายสิบตัว แต่ที่ใช้บ่อยที่สุดในแอปทั่วไปได้แก่:
-
Button — รองรับหลาย variant:
default,destructive,outline,secondary,ghost,linkและหลาย size:sm,default,lg,iconใช้ propasChildเพื่อ Render เป็น Element อื่นได้ เช่น<Link> -
Card — ประกอบด้วย
Card,CardHeader,CardTitle,CardDescription,CardContent,CardFooterใช้จัดวาง Content ในรูปแบบ Card ที่มี shadow และ border radius -
Input — ใช้แทน
<input>ธรรมดา มี style ที่สอดคล้องกับ Design System และรองรับ Accessibility เช่น aria attributes -
Dialog — Modal window ที่ Accessible ประกอบด้วย
Dialog,DialogTrigger,DialogContent,DialogHeader,DialogTitle,DialogDescription,DialogFooterจัดการ Focus Trap และ Keyboard Navigation อัตโนมัติ -
Badge — Tag เล็กๆ สำหรับแสดงสถานะหรือ Label
variant:
default,secondary,destructive,outline -
Table — ประกอบด้วย
Table,TableHeader,TableBody,TableRow,TableHead,TableCellเหมาะสำหรับแสดงข้อมูลรายการ เช่น รายการจอง
โครงสร้าง Card Component
─────────────────────────────────────────────────
<Card> ← wrapper หลัก
<CardHeader> ← ส่วนหัว (title + description)
<CardTitle> ← หัวข้อหลัก
<CardDescription> ← คำอธิบายย่อ
</CardHeader>
<CardContent> ← เนื้อหาหลัก
</CardContent>
<CardFooter> ← ส่วนท้าย (actions)
</CardFooter>
</Card>5. Theming และ CSS Variables ใน Shadcn
Shadcn/ui ใช้ CSS Variables เป็นพื้นฐานของ Theming
ทำให้สามารถสลับ Theme (เช่น Light/Dark mode) ได้โดยไม่ต้องแก้ไข Component โดยตรง
ไฟล์ src/index.css จะมี CSS Variables ที่กำหนดสีหลักทั้งหมด เช่น
--background, --foreground, --primary,
--secondary, --muted, --accent
Tailwind Class ที่ Shadcn ใช้จะอ้างอิงถึง CSS Variables เหล่านี้ เช่น
bg-background = background-color: hsl(var(--background))
text-primary = color: hsl(var(--primary))
ดังนั้นเมื่อเปลี่ยนค่า CSS Variable ทุก Component ที่ใช้ Class นั้นจะเปลี่ยนสีตามอัตโนมัติ
ในการทำ Dark Mode ใน Shadcn เพียงเพิ่ม class dark ที่ <html> element
CSS Variables ใน :root จะถูก override โดย Variables ใน .dark
ซึ่ง Shadcn สร้างให้อัตโนมัติตอน init
CSS Variables ใน index.css (ตัวอย่าง)
─────────────────────────────────────────────────
:root {
--background: 0 0% 100%; ← สีขาว (Light mode)
--foreground: 222.2 84% 4.9%; ← สีเข้ม
--primary: 222.2 47.4% 11.2%; ← สีหลัก
--primary-foreground: 210 40% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
}
.dark {
--background: 222.2 84% 4.9%; ← สีเข้ม (Dark mode)
--foreground: 210 40% 98%; ← สีอ่อน
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
ตอนรัน npx shadcn@latest init ให้เลือก Style: Default
และ Color: Slate เพราะ Slate เป็นสีเทาที่ดูมืออาชีพและเข้ากันได้ดีกับสีอื่น
ถ้าต้องการเปลี่ยน Color Scheme ภายหลัง แก้ CSS Variables ใน src/index.css ได้เลย
โดยอ้างอิงจาก ui.shadcn.com/themes
# snippet 1: ติดตั้ง Tailwind + Shadcn ใน Vite project
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# ติดตั้ง Shadcn
npx shadcn@latest init
# เลือก: Style=Default, Color=Slate, CSS variables=yes
# เพิ่ม components ที่ต้องการ
npx shadcn@latest add button card input dialog badge
// snippet 2: ใช้ Shadcn components ใน ServiceCard
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Link } from '@tanstack/react-router';
function ServiceCard({ id, name, description, price, duration_min, image_url }) {
return (
<Card className="hover:shadow-lg transition-shadow">
{image_url && (
<img src={image_url} alt={name} className="w-full h-48 object-cover rounded-t-lg" />
)}
<CardHeader>
<CardTitle>{name}</CardTitle>
<Badge variant="secondary">{duration_min} นาที</Badge>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">{description}</p>
</CardContent>
<CardFooter className="flex justify-between items-center">
<span className="text-xl font-bold text-primary">{price} ฿</span>
<Button asChild>
<Link to="/services/$serviceId" params={{ serviceId: id }}>จองเลย</Link>
</Button>
</CardFooter>
</Card>
);
}
// snippet 3: Dialog สำหรับ Booking Form
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
function BookingDialog({ service }) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>จองบริการ: {service.name}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>จอง {service.name}</DialogTitle>
</DialogHeader>
<form className="space-y-4">
<Input type="date" name="date" required />
<select className="w-full border rounded px-3 py-2">
<option>09:00</option>
<option>10:00</option>
<option>11:00</option>
</select>
<Button type="submit" className="w-full">ยืนยันการจอง</Button>
</form>
</DialogContent>
</Dialog>
);
}
🧪 ปฏิบัติการ Lab 10 — BookEasy: รีดีไซน์ UI ด้วย Shadcn
นำ Shadcn/ui และ Tailwind CSS มารีดีไซน์หน้าตา BookEasy ทั้งหมดให้สวยงาม สม่ำเสมอ และ Responsive
ต่อยอดจาก Lab 9 — รีดีไซน์ UI ทั้งหมดของ BookEasy ให้สวยงามด้วย Shadcn/ui + Tailwind
ติดตั้ง Tailwind CSS และ Shadcn/ui เข้าสู่โปรเจกต์ BookEasy แล้วเพิ่ม Components ที่จะใช้ในสัปดาห์นี้
- ติดตั้ง tailwindcss, postcss, autoprefixer ตาม snippet 1 ด้านบน
- รัน
npx shadcn@latest initเลือก Style=Default, Color=Slate, CSS variables=yes - เพิ่ม components:
button card input dialog badge table avatarโดยใช้npx shadcn@latest add button card input dialog badge table avatar -
ตรวจสอบว่าโฟลเดอร์
src/components/ui/มีไฟล์ Component ทั้งหมดครบถ้วน -
เพิ่ม Path Alias
@ในvite.config.js:resolve: { alias: { '@': path.resolve(__dirname, './src') } }
รัน npm run dev ได้โดยไม่มี error
โฟลเดอร์ src/components/ui/ มีไฟล์ button.jsx, card.jsx, input.jsx, dialog.jsx, badge.jsx, table.jsx, avatar.jsx
และแอปยังแสดงหน้าเดิมได้ถูกต้อง
ถ้าเกิด error เรื่อง Path Alias ให้ติดตั้ง @types/node ก่อน:
npm install -D @types/node แล้วเพิ่ม import path from 'path'
ที่ด้านบนของ vite.config.js
แทนที่ HTML div ธรรมดาใน ServiceCard ด้วย Component ของ Shadcn ตาม snippet 2
-
เปิดไฟล์
src/components/ServiceCard.jsxแทนที่<div className="service-card">ด้วย<Card>ของ Shadcn -
ใช้
<CardHeader>,<CardTitle>,<CardContent>,<CardFooter>จัดโครงสร้างเนื้อหาใน Card -
ใช้
<Badge variant="secondary">แสดงระยะเวลา (duration_min) -
ใช้
<Button asChild><Link></Link></Button>สำหรับปุ่มจอง เพื่อให้ Render เป็น Link แต่มีหน้าตาเป็น Button -
เพิ่ม Tailwind classes เพื่อ Hover Effect:
hover:shadow-lg transition-shadow
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Link } from '@tanstack/react-router';
export function ServiceCard({ id, name, description, price, duration_min, image_url }) {
return (
<Card className="hover:shadow-lg transition-shadow overflow-hidden">
{image_url ? (
<img src={image_url} alt={name} className="w-full h-48 object-cover" />
) : (
<div className="w-full h-48 bg-muted flex items-center justify-center text-muted-foreground">
ไม่มีรูปภาพ
</div>
)}
<CardHeader>
<CardTitle>{name}</CardTitle>
<Badge variant="secondary">{duration_min} นาที</Badge>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">{description}</p>
</CardContent>
<CardFooter className="flex justify-between items-center">
<span className="text-xl font-bold text-primary">{price} ฿</span>
<Button asChild>
<Link to="/services/$serviceId" params={{ serviceId: id }}>จองเลย</Link>
</Button>
</CardFooter>
</Card>
);
}
หน้าแสดงบริการแสดง ServiceCard ที่มีรูปแบบใหม่ด้วย Shadcn Card แต่ละ Card มี Badge แสดงระยะเวลา และ Button จองเลย เมื่อ Hover ที่ Card จะมี shadow เพิ่มขึ้น
เพิ่ม overflow-hidden ใน <Card>
เพื่อให้รูปภาพ Crop ตาม Border Radius ของ Card ได้ถูกต้อง
ถ้าไม่ใส่ มุมของรูปจะล้นออกนอก Card
แทนที่ Booking Form ธรรมดาด้วย Dialog ของ Shadcn เพื่อ UX ที่ดีขึ้น
-
สร้างไฟล์
src/components/BookingDialog.jsxใช้<Dialog>จาก Shadcn ตาม snippet 3 ด้านบน -
ใช้
<DialogTrigger asChild>ห่อ Button เพื่อให้เปิด Dialog เมื่อกด -
ใช้
<Input>จาก Shadcn แทน<input>ธรรมดา สำหรับ field วันที่ในฟอร์ม -
นำ
<BookingDialog>ไปใช้ใน ServiceCard หรือ Service Detail Page แทนที่ปุ่มหรือ form เดิม
กดปุ่ม "จองบริการ" → Dialog เปิดขึ้นพร้อม Form กด Escape หรือคลิกนอก Dialog → Dialog ปิดลง กรอกวันที่และเวลา → กด "ยืนยันการจอง" → Form ส่งข้อมูลได้
Shadcn Dialog ใช้ Radix UI ภายใน ซึ่งจัดการ Focus Trap และ Keyboard Navigation อัตโนมัติ
ไม่ต้อง Implement เองเหมือน Modal ธรรมดา
ถ้า Dialog ไม่ปิดเมื่อกด Escape ให้ตรวจสอบว่า import ถูก Component จาก '@/components/ui/dialog'
ใช้ Tailwind Classes สร้าง Responsive Navbar และเพิ่ม Avatar แสดงรูปโปรไฟล์ผู้ใช้
-
เปิดไฟล์
src/components/Header.jsx(หรือ Navbar) เพิ่ม Tailwind classes สำหรับ Responsive Layout:flex items-center justify-between px-6 py-4 border-b bg-background -
ใช้
<Avatar>จาก Shadcn แสดงรูปโปรไฟล์ผู้ใช้ ถ้าไม่มีรูป ใช้<AvatarFallback>แสดงตัวอักษรย่อของชื่อแทน -
เพิ่ม Responsive สำหรับ Navigation: ซ่อน Link บน mobile ด้วย
hidden md:flexและแสดง Hamburger Menu แทน (ใช้ state toggle ได้) - ใช้ Tailwind Utility classes แทน CSS custom ที่เคยเขียนไว้เพื่อให้โค้ดสะอาดขึ้น
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Link } from '@tanstack/react-router';
import { useAuth } from '../contexts/AuthContext';
export function Header() {
const { user, signOut } = useAuth();
const initials = user?.email?.slice(0, 2).toUpperCase() ?? 'U';
return (
<header className="flex items-center justify-between px-6 py-4 border-b bg-background">
<Link to="/" className="text-xl font-bold text-primary">
BookEasy
</Link>
<nav className="hidden md:flex items-center gap-4">
<Link to="/services" className="text-sm hover:text-primary transition-colors">บริการ</Link>
<Link to="/bookings" className="text-sm hover:text-primary transition-colors">การจองของฉัน</Link>
</nav>
<div className="flex items-center gap-3">
{user ? (
<>
<Avatar>
<AvatarImage src={user.user_metadata?.avatar_url} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<Button variant="outline" size="sm" onClick={signOut}>ออกจากระบบ</Button>
</>
) : (
<Button asChild size="sm">
<Link to="/login">เข้าสู่ระบบ</Link>
</Button>
)}
</div>
</header>
);
}
Header แสดงถูกต้องทั้งบน Desktop และ Mobile เมื่อ Login แล้วจะเห็น Avatar และปุ่มออกจากระบบ เมื่อยังไม่ Login จะเห็นปุ่มเข้าสู่ระบบ บน Mobile (ความกว้างน้อยกว่า 768px) เมนู Navigation จะซ่อนไป
AvatarFallback แสดงเมื่อ AvatarImage โหลดไม่สำเร็จหรือไม่มี src
ใช้ตัวอักษรย่อชื่อผู้ใช้ เช่น user.email.slice(0, 2).toUpperCase()
เพื่อให้ Fallback ดูมีความหมายแทนที่จะแสดงพื้นที่สีเทาเปล่า