โหมดมืด
บทที่ 6 — Styling + UI Libraries
🟡 ระดับ: กลาง
📖 คำศัพท์รวมของบท (อ่านก่อน):
- styling = จัดหน้าตา/สี/ระยะ/ขนาดของ UI ด้วย CSS
- UI library = ชุด component สำเร็จรูปที่เอามาใช้ได้เลย
- utility class = class เล็กๆ ทำหน้าที่เดียว (เช่น
p-4= padding 16px) — เอาหลายๆ ตัวประกอบกัน- scoped = class ผูกกับไฟล์/component เดียว ไม่ชนกับที่อื่น
- runtime (ของ CSS-in-JS) = สร้าง CSS ตอน app รัน (ช้า, ใช้ memory; และใช้ React Context ใน render ทำให้ไม่ทำงานใน React Server Components) ↔ zero-runtime = สร้าง CSS ตอน build (เร็ว, ใช้กับ RSC ได้)
- headless library = ให้แค่ "พฤติกรรม" (เปิด/ปิด, focus, keyboard) ไม่ให้ "หน้าตา" — เราเขียน CSS เอง
- primitive (ของ Radix) = component พื้นฐานที่ไม่มี style — เอาไปแต่ง/ประกอบเอง
- design system = ชุดของ component + token + กฎที่ใช้ทั้ง product
- a11y (accessibility) = ออกแบบให้ใช้ keyboard อย่างเดียวได้, screen reader อ่านได้, contrast พอ ฯลฯ
- SPA (Single Page Application) = app ที่ browser โหลด HTML ครั้งเดียว แล้ว JavaScript จัดการ routing + rendering ทั้งหมด — Vite ทำแบบนี้โดย default
- SSR (Server-Side Rendering) = server วาด HTML ก่อนส่งให้ client (Next.js — บท 11)
- hydration = "เติมชีวิต" ให้ HTML ที่ server ส่งมา → กลายเป็น React app ที่กดได้
- RSC = React Server Components (บท 11)
- FOUC = Flash of Unstyled Content (เนื้อหากระพริบตอนโหลด CSS ไม่ทัน)
💡 ถ้ายังไม่รู้ว่า RSC/SSR คืออะไร — ไม่ต้องกังวล ตอนนี้รู้แค่ว่า "CSS-in-JS แบบ runtime มีปัญหากับ feature ขั้นสูงของ React" ก็พอ ไปต่อได้เลย (รายละเอียดอยู่บทที่ 11)
บทนี้ตอบคำถามที่ทุกคนเจอตอนเริ่ม React: "จะ style ยังไงดี?"
ปี 2026 มีตัวเลือกเยอะ — บทนี้พาดูทีละแบบ + แนะนำที่ควรใช้
หลังจบบท คุณจะ:
- เข้าใจ trade-off (การได้อย่างเสียอย่าง — ข้อดี vs ข้อเสีย) ของ CSS approach แต่ละแบบ
- ใช้ Tailwind CSS ได้คล่อง (แนวทางหลักปี 2026)
- รู้จัก CSS Modules, CSS-in-JS, Panda CSS
- เลือก component library ได้ — Material UI / Chakra / shadcn/ui / Radix / Headless UI
- เข้าใจ "Headless UI" (UI ที่ให้แค่ logic/พฤติกรรม ไม่บังคับหน้าตา เราแต่งเอง) vs "Styled UI" (มาพร้อมหน้าตาสำเร็จ)
1. ตัวเลือก CSS ใน React
| Approach | สรุป |
|---|---|
| Plain CSS | global, manual class — เก่า แต่ใช้ได้ |
| CSS Modules | scoped class (auto-rename) — ปลอดภัย |
| Tailwind CSS | utility classes — ⭐ standard ปี 2026 |
| CSS-in-JS (styled-components, Emotion) | เขียน CSS ใน JS — เคยฮิต ตอนนี้ลด |
| Panda CSS | zero-runtime CSS-in-JS แบบใหม่ |
| CSS framework (Bootstrap) | utility + component — ไม่ใช้กับ React มาก |
คำแนะนำสำหรับปี 2026:
- Tailwind CSS + shadcn/ui → 80% ของ project ใหม่ (shadcn/ui = ชุด component แต่ "copy โค้ดมาวางในโปรเจกต์เอง" ไม่ install ผ่าน npm — ดูรายละเอียดที่ Part 9: shadcn/ui ด้านล่าง)
- CSS Modules ถ้าทีมไม่ชอบ Tailwind
- MUI / Chakra ถ้าต้องการ design system สำเร็จรูป
⚠️ CSS-in-JS แบบ runtime กำลังตาย (ปี 2024-2025):
- styled-components ประกาศ "in maintenance mode" (โหมดดูแลเฉย ๆ — ไม่มี feature ใหม่) ช่วงต้นปี 2025 (ตรวจสอบวันที่แน่นอนจาก changelog/GitHub ของ styled-components ก่อนอ้างอิง)
- Emotion runtime (runtime = สร้าง CSS ตอน app ทำงาน — กิน CPU/memory) ไม่เข้ากับ React Server Components เพราะใช้ React Context ใน render → ใช้ใน Server Component ไม่ได้
- คนย้ายไป Tailwind, CSS Modules, หรือ zero-runtime (สร้าง CSS ตอน build แล้ว ไม่กิน runtime) อย่าง Panda CSS, Pigment CSS, vanilla-extract แทน
- สรุป: ถ้าเริ่มโปรเจกต์ใหม่ปี 2026 — อย่าเลือก styled-components/Emotion ถ้าวางแผนใช้ RSC
Part 1: Plain CSS — รากฐาน
2. Global CSS
css
/* src/styles.css */
.button {
padding: 8px 16px;
background: blue;
color: white;
border-radius: 4px;
}
.button-large {
padding: 12px 24px;
}tsx
// main.tsx — เปิดไฟล์นี้แล้วเพิ่ม import CSS ไว้ด้านบน (ก่อน import อื่นๆ ก็ได้)
import './styles.css'; // ← เพิ่มบรรทัดนี้
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);ใช้ใน component:
tsx
<button className="button">Click</button>
<button className="button button-large">Bigger</button>ปัญหา:
.buttonทั่ว app เห็นหมด → ชน → bug- ไม่มี autocomplete
- หา class ที่ใช้/ไม่ใช้ยาก
Part 2: CSS Modules — scoped class
3. ใช้งาน
css
/* Button.module.css */
.button {
padding: 8px 16px;
background: blue;
color: white;
}
.large {
padding: 12px 24px;
}💡 อ่านโค้ดข้างล่างไม่ออกทั้งหมด? ไม่เป็นไร —
{ large }: { large?: boolean }คือ syntax ของ TypeScript สำหรับกำหนด "type ของ prop" (เคยเจอมาแล้วในบทก่อนๆ) ส่วนที่ต้องสนใจตอนนี้คือ?ตัวเดียว = optional prop (ส่งหรือไม่ส่งก็ได้)
tsx
import styles from './Button.module.css';
function Button({ large }: { large?: boolean }) { // `?` = optional prop — ส่งหรือไม่ส่งก็ได้
return (
<button className={`${styles.button} ${large ? styles.large : ''}`}>
Click
</button>
);
}Build ออกมา class จะถูก rename:
html
<button class="Button_button__abc12 Button_large__def34">→ scoped = ใช้ชื่อ .button ในไฟล์อื่นไม่ชน
Pros / Cons
✅ Scoped — no collision
✅ Standard CSS — เรียนรู้ง่าย
❌ ต้องสร้างไฟล์ .module.css ทุก component
❌ class composition (การประกอบหลาย class เข้าด้วยกัน — เช่น base + variant) ยุ่ง
Part 3: Tailwind CSS — Utility-First
4. แนวคิด — เลิกตั้งชื่อ class
แทนที่จะ:
css
.card { padding: 16px; margin: 8px; border: 1px solid gray; border-radius: 4px; }ใช้ utility class:
tsx
<div className="p-4 m-2 border border-gray-300 rounded">
...
</div>ดูเหมือนเลอะตอนแรก — แต่:
- ไม่ต้องตั้งชื่อ class (เลิกคิด
card,card-header,card-body) - ไม่ต้องสลับไฟล์ HTML ↔ CSS
- ไม่มี dead CSS (CSS ที่เขียนไว้แต่ไม่มี class ไหนใช้ — กินขนาด bundle เปล่า ๆ); Tailwind ลบ class ที่ไม่ใช้ตอน build
- design system consistent (
p-4=padding: 16pxเท่ากันทั้ง app)
5. Setup (Vite + React)
⚠️ Prerequisite: สมมติว่าคุณมีโปรเจกต์ Vite + React + TypeScript พร้อมแล้ว (จากบทที่ 1) — ถ้ายังไม่มี ให้สร้างก่อนด้วย
npm create vite@latest my-app -- --template react-tsแล้วเข้าโฟลเดอร์โปรเจกต์ก่อนรัน install
bash
npm install tailwindcss @tailwindcss/vite⚠️ Prerequisite: Tailwind v4 ต้องใช้ Node 20+ และ
@tailwindcss/viteต้องการ Vite ≥ 5.2.0 — ถ้า Vite เก่ากว่านี้ install จะ fail ให้npm install vite@latestก่อน
ts
// vite.config.ts — แทนที่ไฟล์ทั้งหมดด้วยโค้ดนี้ (react plugin มาจาก @vitejs/plugin-react ที่ Vite สร้างมาให้แล้ว ไม่ต้อง install เพิ่ม)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});css
/* src/index.css */
@import "tailwindcss";tsx
// main.tsx
import './index.css';ปี 2026 ใช้ Tailwind v4 — config น้อยลงมาก (zero-config)
⭐ Tailwind v4 ต่างจาก v3 ยังไง
| v3 | v4 (2024+) | |
|---|---|---|
| Config | tailwind.config.js (JS) | CSS-first ใน @theme block |
| Engine | PostCSS plugin | Oxide (Rust) — เร็วขึ้นมาก (official benchmark ~5-10x แต่ผลจริงต่างกันตามโปรเจกต์ — ดู benchmark จริงในประกาศทางการ Tailwind v4) |
| Install | tailwindcss postcss autoprefixer + config files | แค่ tailwindcss @tailwindcss/vite |
| Import | @tailwind base; @tailwind components; @tailwind utilities; | @import "tailwindcss"; |
| Content scan | content: ['./src/**'] ใน config | auto-detect |
| Custom color/spacing | extend ใน config | @theme { --color-brand: #f00 } |
ตัวอย่าง custom theme ใน v4:
css
/* index.css */
@import "tailwindcss";
@theme {
--color-brand: oklch(0.7 0.2 250); /* oklch() = วิธีระบุสีแบบใหม่ที่ Tailwind v4 ใช้ — จะใช้ hex ปกติเช่น #4f46e5 ก็ได้ */
--color-brand-light: oklch(0.85 0.15 250);
--spacing-page: 1.5rem;
--font-display: "Inter", sans-serif;
}💡 ไม่ต้องตกใจกับ
oklch(...)— มันคือ "วิธีระบุสีแบบใหม่" (กำหนดความสว่าง/ความเข้ม/เฉดสีแยกกัน) ที่ Tailwind v4 ชอบใช้ ถ้ายังไม่คุ้น ใช้สี hex ปกติ (#4f46e5) หรือrgb(...)แทนได้เลย ผลลัพธ์เหมือนกัน
ใช้ใน class ปกติ:
tsx
<div className="bg-brand text-white p-page font-display">...</div>Container Queries (v4 built-in)
ก่อน — responsive design ใช้แค่ viewport size:
tsx
<div className="md:grid-cols-2 lg:grid-cols-3"> {/* responsive ตาม viewport */}ปี 2026 — Container queries (query ที่ดูขนาด container/parent ไม่ใช่ viewport ทั้งจอ — ทำให้ component reusable ใช้ใน sidebar/main ได้) ให้ component responsive ตาม parent ของมัน (ไม่ใช่ viewport)
tsx
<div className="@container"> {/* mark parent */}
<div className="@md:grid-cols-2 @lg:grid-cols-3"> {/* responsive ตาม @container */}
...
</div>
</div>→ component reusable: ใส่ใน sidebar (แคบ) หรือ main (กว้าง) แสดงต่างกันได้
6. Utility ที่ใช้บ่อย
ที่ใช้จริงในงานประจำมีไม่กี่กลุ่ม:
Layout
tsx
<div className="flex items-center justify-between gap-4">
<div className="grid grid-cols-3 gap-2">
<div className="w-full max-w-md mx-auto">Spacing (scale 0-96)
text
p-2 = padding 0.5rem (8px)
p-4 = padding 1rem (16px)
px-4 py-2 = ซ้ายขวา 16px บนล่าง 8px
m-2, my-4, ml-1 ...
gap-4, space-x-2Typography
tsx
<h1 className="text-2xl font-bold">
<p className="text-sm text-gray-600">
<span className="font-mono">Color (สี + เฉด 50-950)
tsx
<div className="bg-blue-500 text-white">
<div className="bg-red-100 text-red-900 border border-red-300">State (hover, focus, active, disabled)
tsx
<button className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:opacity-50">Responsive (mobile-first)
tsx
<div className="w-full md:w-1/2 lg:w-1/3">
{/* mobile: 100%, ≥768px: 50%, ≥1024px: 33% */}Breakpoints default:
text
sm: 640px md: 768px lg: 1024px xl: 1280px 2xl: 1536pxDark mode
tsx
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">7. ตัวอย่างเต็ม — Card Component
tsx
type Props = { title: string; description: string; image: string };
function Card({ title, description, image }: Props) {
return (
<div className="max-w-sm rounded-lg overflow-hidden shadow-lg bg-white dark:bg-gray-800">
<img className="w-full h-48 object-cover" src={image} alt={title} />
<div className="p-6">
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{title}
</h3>
<p className="text-gray-700 dark:text-gray-300 text-sm">
{description}
</p>
<button className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors">
Read more
</button>
</div>
</div>
);
}8. ⚠️ Tailwind Pitfalls
1. Class ที่ generate dynamic ไม่ทำงาน
tsx
// ❌ Tailwind scan source file หา class — ตอน build เห็น `bg-${color}-500` ไม่ใช่ class จริง
<div className={`bg-${color}-500`}>
// ✅ ใช้ map ที่ static
const colors = {
red: 'bg-red-500',
blue: 'bg-blue-500',
green: 'bg-green-500',
};
<div className={colors[color]}>2. Class ยาวเกิน → อ่านยาก
tsx
<div className="flex items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md transition-shadow">แก้:
- แยก component ที่เล็กลง
- ใช้
clsx+tailwind-merge(รวมเป็นcn()util) - ใช้ shadcn/ui (เห็นด้านล่าง)
tsx
// หมายเหตุ: `@/` คือ path alias ของ `src/` — ต้อง config ใน vite.config.ts (resolve.alias) + tsconfig.json (paths)
// ⚠️ ตัวอย่างนี้ใช้ได้ทันทีถ้าโปรเจกต์รัน `npx shadcn init` แล้ว (shadcn setup alias `@/` ให้อัตโนมัติ)
// ถ้ายังไม่ได้รัน shadcn init — import `@/lib/utils` จะ error "Cannot find module" เพราะ alias ยังไม่ถูก config
// ไฟล์ `src/lib/utils.ts` นี้ shadcn สร้างให้อัตโนมัติตอน `npx shadcn init`
// ถ้าไม่ใช้ shadcn สร้างเองด้วย: npm install clsx tailwind-merge แล้ว export cn ดังนี้:
// export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
import { cn } from '@/lib/utils';
import type { ComponentProps } from 'react';
type CardProps = ComponentProps<'div'>;
function Card({ className, ...props }: CardProps) {
return (
<div className={cn(
'p-4 rounded-lg shadow',
'bg-white dark:bg-gray-800',
'hover:shadow-md transition-shadow',
className
)} {...props} />
);
}3. Override styles
Tailwind utility = specificity (น้ำหนัก/ความเฉพาะของ CSS selector — กฎที่กำหนดว่า class ไหนชนะกัน) เท่ากัน → ลำดับใน HTML สำคัญ
ใช้ tailwind-merge เพื่อจัดการ:
tsx
cn('p-4 bg-red-500', 'bg-blue-500') // → "p-4 bg-blue-500" (override)Part 4: Panda CSS — Zero-runtime CSS-in-JS
9. แนวคิด
Panda = "เขียน TypeScript object → generate CSS file"
tsx
import { css } from '../styled-system/css';
<button className={css({
px: 4,
py: 2,
bg: 'blue.500',
color: 'white',
rounded: 'md',
_hover: { bg: 'blue.600' }
})}>
Click
</button>ตอน build → CSS file ที่ pre-generated → ไม่มี runtime cost (ต่าง styled-components/Emotion)
ใช้น้อยกว่า Tailwind — แต่ดีถ้าทีมชอบ object syntax + type-safety
Part 5: Component Library Strategy
10. 3 แนวคิด
A. Styled UI Library (MUI, Chakra, Ant Design)
ได้ component + ดีไซน์สำเร็จรูป
ปัญหา: ดีไซน์ผูกกับ library → customize ยาก, lock-in (ติดกับ library — อยากเปลี่ยนยากเพราะต้อง migrate ทั้ง app)
B. Headless UI Library (Radix, Headless UI, React Aria)
ได้ component logic (open/close, focus, accessibility) — ไม่มี style
เรา style เอง (ด้วย Tailwind)
C. shadcn/ui — ⭐ ปี 2026 มาตรฐาน
ไม่ใช่ library — เป็น collection ของ component ที่เรา copy ลง project
Built on: Radix UI + Tailwind
เป็นเจ้าของ source — แก้ได้ตามใจ
Part 6: Material UI (MUI) — Styled
11. ใช้งาน
bash
npm install @mui/material @emotion/react @emotion/styledtsx
import { Button, TextField, Card, CardContent, Typography } from '@mui/material';
function App() {
return (
<Card sx={{ maxWidth: 400 }}>
<CardContent>
<Typography variant="h5">Sign up</Typography>
<TextField label="Email" fullWidth margin="normal" />
<TextField label="Password" type="password" fullWidth margin="normal" />
<Button variant="contained" fullWidth>Submit</Button>
</CardContent>
</Card>
);
}ข้อดี: component เยอะ (50+), ดีไซน์สวย, accessibility ดี
ข้อเสีย: bundle ใหญ่, customize CSS ลำบาก (sx prop / styled API)
💡 MUI v6 เพิ่มรองรับ Pigment CSS (zero-runtime CSS-in-JS) เป็น optional alternative — แต่ Emotion ยังคงเป็น default ใน v6 ที่ release แล้ว ไม่ได้เปลี่ยนอัตโนมัติ หากต้องการ Pigment CSS ต้อง opt-in เอง ⚠️ หมายเหตุ: เวอร์ชัน MUI ล่าสุดเปลี่ยนเร็ว — เช็คเวอร์ชันจริงด้วย
npm view @mui/material versionและดู changelog ก่อน upgrade เสมอ เพราะมักมี breaking changes
Part 7: Chakra UI — Styled, ใช้งานง่าย
bash
npm install @chakra-ui/react @emotion/reacttsx
import { Box, Button, Heading, Stack, Input } from '@chakra-ui/react';
<Box p={8} maxW="md" borderWidth={1} borderRadius="lg">
<Heading size="md">Sign up</Heading>
<Stack spacing={4} mt={4}>
<Input placeholder="Email" />
<Input placeholder="Password" type="password" />
<Button colorScheme="blue">Submit</Button>
</Stack>
</Box>ข้อดี: prop-based styling (ดีกว่า sx), accessibility ดี
ข้อเสีย: bundle ใหญ่, design lock-in
💡 Chakra v3 (2024) มี breaking changes ใหญ่: ชื่อ Provider เปลี่ยนเป็น
<Provider>(ยังต้องครอบ app เหมือนเดิม ไม่ใช่ auto-mount), theming เปลี่ยนเป็น recipe-based (slot recipes), โครงสร้าง import เปลี่ยนหมด — ดู migration guide ก่อน upgrade จาก v2
Part 8: Radix UI — Headless
12. Radix Primitives — สำหรับ accessibility ที่ยาก
bash
npm install @radix-ui/react-dialogtsx
import * as Dialog from '@radix-ui/react-dialog';
function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger className="px-4 py-2 bg-blue-500 text-white rounded">
Open
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded shadow-lg">
<Dialog.Title className="text-xl font-bold">Confirm</Dialog.Title>
<Dialog.Description className="mt-2 text-gray-600">
Are you sure?
</Dialog.Description>
<div className="mt-4 flex gap-2 justify-end">
<Dialog.Close className="px-3 py-1 bg-gray-200 rounded">Cancel</Dialog.Close>
<button className="px-3 py-1 bg-red-500 text-white rounded">Confirm</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}Radix จัดการ:
- Focus trap (กักโฟกัสไว้ใน dialog — กด Tab วนในกรอบ ไม่หลุดออกไปหน้าหลัง)
- ESC ปิด
- Click outside ปิด
- ARIA attributes (ป้ายชื่อสำหรับ screen reader — ช่วยคนพิการใช้งานได้)
- Keyboard navigation (ใช้ keyboard แทน mouse ได้)
เรา style เอง = อิสระเต็มที่
Part 9: shadcn/ui — ⭐ มาตรฐานปี 2026
13. ทำไม shadcn/ui เปลี่ยนเกม
ก่อน — npm install ui-library → ได้ library ที่ลบไม่ได้, customize ยาก, ฝัง runtime CSS
shadcn — รัน CLI → copy code component ลงโปรเจกต์เรา → เป็นเจ้าของ source
💡 เทียบให้เห็นภาพ:
- MUI/Chakra = ซื้อโซฟาสำเร็จมาวางบ้าน → สวย ใช้ได้ทันที แต่จะทาสีโซฟาใหม่ลำบาก ต้อง "ขอ" ทางบริษัทผ่านช่องที่เขากำหนดให้เท่านั้น (ในทางเทคนิคคือ override ผ่าน prop/theme API ของ library — แก้ตรงๆ ไม่ได้)
- shadcn = CLI copy ไฟล์
button.tsxลงsrc/components/ui/ของเรา ครั้งเดียว → ไฟล์เป็นของเรา แก้บรรทัดไหนก็ได้ ลบ variant ที่ไม่ใช้ก็ได้ข้อดี: อิสระสุด — แก้/ลบได้ตามใจ ข้อเสีย: ไม่มี
npm update— ถ้า shadcn ปล่อย button รุ่นใหม่ ต้องnpx shadcn@latest add buttonทับเอง (หรือไม่ update ก็ได้ ของเราใช้ปกติ)
bash
# npx = รัน CLI tool โดยไม่ต้อง install เก็บไว้ในเครื่อง (ดาวน์โหลด + รัน ครั้งเดียวจบ) ต่างจาก npm install
# package เปลี่ยนชื่อจาก shadcn-ui → shadcn (ตั้งแต่ปี 2024)
npx shadcn@latest init
npx shadcn@latest add button card dialog💡
npx shadcn@latest initจะถาม interactive: TypeScript? (yes), style? (Default), base color? (Slate), CSS variables for colors? (yes), path alias@/*? (yes) — มือใหม่เลือก default ทั้งหมดได้เลย กด Enter ผ่านทุกข้อ
ได้:
text
src/
└── components/
└── ui/
├── button.tsx ← code ของเรา (Radix + Tailwind + cva)
├── card.tsx
└── dialog.tsxใช้:
tsx
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>Hello</CardTitle>
</CardHeader>
<CardContent>
<Button variant="default">Click</Button>
<Button variant="destructive">Delete</Button>
</CardContent>
</Card>Variant System (cva)
📖 cva (class-variance-authority) = library สร้าง variant ของ component ด้วย Tailwind class อย่างเป็นระบบ — shadcn install ให้อัตโนมัติตอนรัน
npx shadcn@latest initไม่ต้อง install เพิ่ม
⚠️ หมายเหตุ: class อย่าง
bg-primary,bg-destructive,bg-background,bg-accentด้านล่างไม่ใช่สี Tailwind ปกติ — เป็น CSS variable ที่ shadcn กำหนดไว้ใน theming (ดูรายละเอียดที่ §14 ด้านล่าง) ถ้า copy โค้ดนี้ไปใช้ในโปรเจกต์ Tailwind เปล่าที่ไม่ได้ผ่านnpx shadcn initจะไม่มีสีขึ้น (class ไม่มี effect เพราะ variable ไม่ถูกประกาศ ไม่มี error ให้เห็น)
tsx
// button.tsx (ตัวอย่างจาก shadcn)
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-white hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-12 px-8 text-base',
}
},
defaultVariants: { variant: 'default', size: 'default' }
}
);
<Button variant="outline" size="lg">Click</Button>Why ⭐
✅ Source อยู่ที่เรา — แก้ได้ทุกอย่าง
✅ Built on Radix (accessibility ดี) + Tailwind
✅ Type-safe variant ด้วย cva (shadcn กำลัง migrate บาง component ไป tailwind-variants ในอนาคต แต่ cva ยังใช้งานได้ปกติ)
✅ Tree-shake แท้จริง (เขย่า tree เอาใบไม้แห้งทิ้ง — ตัดโค้ดที่ไม่ใช้ออกจาก bundle) — component ที่ไม่ใช้ไม่อยู่ใน bundle
💡 tree-shaking = ตัดโค้ดที่ไม่ได้ใช้ออกตอน build เหมือนเขย่าต้นไม้ให้ใบแห้งร่วง bundle = ไฟล์รวมที่ส่งให้ browser
✅ ดีไซน์ดี + Theming ง่าย (CSS variables)
14. shadcn — Theming
css
/* index.css */
/* ⚠️ ตัวอย่างนี้เป็น HSL format รุ่นเก่า (ก่อนปลายปี 2024)
หมายเหตุ: รูปแบบ "0 0% 100%" คือ Tailwind HSL channel syntax — Tailwind อ่านแล้วใส่ใน hsl() ให้อัตโนมัติ ไม่ใช่ CSS ปกติ
⭐ npx shadcn@latest init ปี 2024+ จะได้ oklch format แทน (ดูตัวอย่างด้านล่าง) */
@layer base {
:root {
--background: 0 0% 100%; /* HSL channel: hue=0, sat=0%, light=100% */
--foreground: 222.2 47.4% 11.2%;
--primary: 221 83% 53%;
--primary-foreground: 0 0% 98%;
/* ... */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
}
}💡 shadcn 2024+ ใหม่ใช้ oklch format: หากรัน
npx shadcn@latest initตอนนี้จะได้ format แบบนี้แทน:css:root { --background: oklch(1 0 0); --primary: oklch(0.623 0.214 259.8); }ทั้งสองแบบใช้งานได้ — แค่อย่าผสมกันในโปรเจกต์เดียว
เปลี่ยน theme ทั้ง app = แก้ CSS variables → ทุก component update อัตโนมัติ
Part 10: Component อื่นที่ควรรู้
15. React Aria (Adobe)
Headless แบบ Radix — focus ที่ accessibility สูงสุด (เหมาะกับ enterprise)
16. Ark UI
ใหม่ — wrapper ของ Zag.js — accessibility-first headless, ทำงานข้าม framework (React/Vue/Solid) ด้วย logic ชุดเดียว
17. Headless UI (Tailwind Labs)
ทำโดยทีม Tailwind — น้อย component แต่ minimal
⚠️ ตัวอย่างด้านล่างเป็น v1 syntax (ใช้ dot-notation เช่น
Dialog.Panel,Dialog.Title) Headless UI v2 (2024) เปลี่ยน API เป็น composition แบบใหม่ ถ้า install version ล่าสุดจะได้ error — ดู official docs สำหรับ v2
tsx
// Headless UI v1 syntax (อ้างอิงเท่านั้น — ดู official docs สำหรับ v2)
import { Dialog } from '@headlessui/react';
<Dialog open={open} onClose={setOpen}>
<Dialog.Panel className="...">
<Dialog.Title>Confirm</Dialog.Title>
</Dialog.Panel>
</Dialog>Part 11: Icon Libraries
| Library | จุดเด่น |
|---|---|
| Lucide React | shadcn ใช้ตัวนี้ — สวย + เบา |
| Heroicons | Tailwind Labs — minimal |
| react-icons | รวมทุก icon set (Font Awesome, Material, Bootstrap, ...) |
| Tabler Icons | สวย + เยอะมาก |
| Iconify | universal — เรียก icon set ใดก็ได้ผ่าน 1 component |
tsx
import { Search, X, ChevronRight } from 'lucide-react';
<Search className="w-5 h-5" />
<X className="w-4 h-4 text-gray-500" />Part 12: Best Practice Stack ปี 2026
Stack ที่ผมแนะนำมือใหม่:
text
Vite + React + TypeScript
├── Tailwind CSS v4 ← styling
├── shadcn/ui ← component
│ └── Radix UI ← primitive (อัตโนมัติ)
├── Lucide React ← icons
├── React Hook Form + Zod ← forms
├── TanStack Query ← data fetching
└── React Router ← routingโปรเจกต์ใหม่ — install ตัวนี้ใน 5 นาที → ทำงานได้
Part 13: ⚠️ Common Pitfalls
| Pitfall | แก้ |
|---|---|
| Mix Tailwind + CSS-in-JS | เลือก 1 — สับสน |
| ใช้ component library 2-3 ตัวพร้อมกัน | เลือก 1 ระบบ |
| Tailwind class ยาวเกิน → ไม่ refactor | ใช้ cn() + แยก component |
Override MUI / Chakra ด้วย !important | ใช้ theme/styled API |
Style ใน inline style={{...}} ทุกที่ | ใช้ class — performance + consistency |
| ไม่ดู accessibility (a11y) | ใช้ Radix / Headless UI — ฟรี a11y |
17.5 Dark Mode + SSR — กับดักที่ทุกคนเจอ
🔴 ข้ามได้ถ้ายังใช้ Vite + SPA — section นี้สำหรับ Next.js/Remix (SSR) เท่านั้น SPA = Single Page Application = app ที่ browser โหลดไฟล์ HTML ครั้งเดียว แล้ว JavaScript จัดการทุกอย่างต่อ (routing, rendering) — ซึ่งเป็นแบบที่ Vite ทำโดย default สำหรับ Vite SPA ใช้ Tailwind
dark:ปกติ + toggle ผ่านclassบน<html>ในuseEffectพอ ไม่กระพริบเพราะไม่มี server render HTML มาก่อน — ทุกอย่างเริ่มจาก JS ตั้งแต่ต้น จึงไม่มี mismatch ระหว่าง server/client
dark mode ใน SSR (Next.js, Remix) มีปัญหา FOUC (Flash of Unstyled Content = หน้ากระพริบเพราะ CSS โหลดไม่ทัน). เหตุผล: server ไม่รู้ว่า user ชอบ dark/light → render เป็น light ก่อน → JS โหลด → switch เป็น dark → กระพริบ 1 frame
ทาง 1 — next-themes (มาตรฐานสำหรับ Next.js)
tsx
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}tsx
// app/layout.tsx
<html lang="th" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>ตัว suppressHydrationWarning (สั่งให้ React ไม่เตือนว่า server กับ client ไม่ตรง — เป็นไปได้ในกรณีนี้) ใส่ที่ <html> เพราะ next-themes แอบใส่ script ใน <head> เพื่ออ่าน localStorage แล้ว set class="dark" ก่อน hydration (เรื่อง hydration ดูบท 11) → server กับ client ไม่ตรง 100% (ตั้งใจ)
ทาง 2 — เขียน script เองใน <head> (แบบ manual)
html
<script>
(function () {
const saved = localStorage.getItem('theme');
const dark = saved === 'dark' || (!saved && matchMedia('(prefers-color-scheme: dark)').matches);
if (dark) document.documentElement.classList.add('dark');
})();
</script>วาง ก่อน stylesheet/React script → set class ก่อนหน้า paint → ไม่กระพริบ
กฎ:
- อย่าใช้
useState(defaultTheme)+useEffectset class ทีหลัง → กระพริบเสมอ - อย่าอ่าน
matchMediaใน render ของ Server Component → server ไม่มี → mismatch - Tailwind v4 dark mode → ใช้
classstrategy (ไม่ใช่media) เพื่อให้ user override ได้
18. Checkpoint
📝 หมายเหตุ: checkpoint บทนี้ ตั้งใจไม่มีเฉลย ให้ลองทำเองก่อน ถ้าติดให้ย้อนไปดูตัวอย่างโค้ดในบทแล้วดัดแปลง
🛠️ Checkpoint 6.1 — Setup shadcn
สร้าง Vite + React + TypeScript project → install Tailwind v4 → init shadcn → add button + card + dialog → ทำหน้า "User profile" ที่ใช้ทั้งสาม
💡 ถ้า shadcn init error: shadcn ต้องการ path alias
@/ซึ่งต้อง config เอง — ดูขั้นตอนครบถ้วนที่ https://ui.shadcn.com/docs/installation/vite
🛠️ Checkpoint 6.2 — Dark Mode
เพิ่ม dark mode toggle (next-themes หรือ manual) → component shadcn ทุกตัวต้องดูดีทั้ง 2 mode
🛠️ Checkpoint 6.3 — Responsive Layout
ทำ Pinterest-style grid:
- mobile: 1 column
- tablet: 2 columns
- desktop: 3-4 columns
- ใช้ Tailwind grid + responsive prefix
🛠️ Checkpoint 6.4 — Custom Button Variants
แก้ button.tsx (จาก shadcn) เพิ่ม variant gradient ที่ background เป็น gradient + variant glow ที่มี shadow สีฟ้า
19. สรุปบท
✅ Tailwind CSS = standard ปี 2026 — utility-first, no naming, auto-purge
✅ CSS Modules = ทางเลือกถ้าไม่ชอบ Tailwind — scoped CSS แท้
✅ ใช้ cn() (clsx + tailwind-merge) จัดการ class composition
✅ shadcn/ui = ⭐ - ไม่ใช่ library, copy code ลง project (Radix + Tailwind)
✅ Radix UI / React Aria / Headless UI = headless — ได้ logic + a11y, style เอง
✅ MUI / Chakra = สำหรับโปรเจกต์ที่ต้องการ design system สำเร็จ (แต่ lock-in)
✅ Icon → Lucide React เข้าคู่ shadcn