Skip to content

บทที่ 6 — Styling + UI Libraries

← บทที่ 5 | สารบัญ | บทที่ 7 →

🟡 ระดับ: กลาง

📖 คำศัพท์รวมของบท (อ่านก่อน):

  • 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 CSSglobal, manual class — เก่า แต่ใช้ได้
CSS Modulesscoped class (auto-rename) — ปลอดภัย
Tailwind CSSutility classes — ⭐ standard ปี 2026
CSS-in-JS (styled-components, Emotion)เขียน CSS ใน JS — เคยฮิต ตอนนี้ลด
Panda CSSzero-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)
  • ไม่ต้องสลับไฟล์ HTMLCSS
  • ไม่มี 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 ยังไง

v3v4 (2024+)
Configtailwind.config.js (JS)CSS-first ใน @theme block
EnginePostCSS pluginOxide (Rust) — เร็วขึ้นมาก (official benchmark ~5-10x แต่ผลจริงต่างกันตามโปรเจกต์ — ดู benchmark จริงในประกาศทางการ Tailwind v4)
Installtailwindcss postcss autoprefixer + config filesแค่ tailwindcss @tailwindcss/vite
Import@tailwind base; @tailwind components; @tailwind utilities;@import "tailwindcss";
Content scancontent: ['./src/**'] ใน configauto-detect
Custom color/spacingextend ใน 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-2

Typography

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: 1536px

Dark 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/styled
tsx
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/react
tsx
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-dialog
tsx
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 — รัน CLIcopy 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 Reactshadcn ใช้ตัวนี้ — สวย + เบา
HeroiconsTailwind Labs — minimal
react-iconsรวมทุก icon set (Font Awesome, Material, Bootstrap, ...)
Tabler Iconsสวย + เยอะมาก
Iconifyuniversal — เรียก 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) + useEffect set class ทีหลัง → กระพริบเสมอ
  • อย่าอ่าน matchMedia ใน render ของ Server Component → server ไม่มี → mismatch
  • Tailwind v4 dark mode → ใช้ class strategy (ไม่ใช่ 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


← บทที่ 5 | บทที่ 7 → State Management