ตรวจด้วยอะไร — ทุกก้อน CSS ในบทนี้ไม่ได้พิมพ์จากความจำ แต่ดึงมาจากไฟล์ CSS ที่โปรเจกต์สร้างขึ้นจริง
(ยิง curl ไปที่ไฟล์ .css ที่ Next.js เสิร์ฟ แล้วอ่านกฎในนั้นตรง ๆ)
โปรเจกต์ที่ใช้ตรวจ สร้างด้วย npx create-next-app@latest --ts --tailwind --eslint --app --no-src-dir
| ของ | เวอร์ชันที่ตรวจ |
|---|---|
next | 16.3.4 (Turbopack) |
react / react-dom | 19.2.8 |
tailwindcss | 4.3.3 |
@tailwindcss/postcss | 4.3.3 |
typescript | 5 |
node | v24.14.0 |
npx tsc --noEmit ผ่าน (exit 0) และ npx next build ขึ้น
✓ Compiled successfully ทุกรอบ (ตัวเลขมิลลิวินาทีที่ต่อท้ายจะไม่เท่ากันทุกครั้ง อย่าไปยึด)
1 · ปัญหาที่ Tailwind มาแก้ ไม่ใช่ปัญหา "CSS ยาก"
ย้อนกลับไปที่บท 59–62 คุณทำแบบนี้เพื่อจะได้ปุ่มหนึ่งปุ่ม:
/* styles.css — ไฟล์หนึ่ง */
.primary-button {
padding: 6px 10px;
border-radius: 6px;
border: 2px solid transparent;
}
<!-- index.html — อีกไฟล์หนึ่ง -->
<button class="primary-button">กดฉัน</button>
โค้ดนี้ไม่มีอะไรผิดเลย มันทำงานได้ดี — แต่พอโปรเจกต์โตขึ้น มันเรียกเก็บ "ค่าธรรมเนียม" สามอย่างจากคุณทุกวัน:
- คุณต้องคิดชื่อ —
.primary-button?.btn-main?.button--primary? ชื่อไม่มีผลต่อหน้าเว็บเลยสักนิด แต่กินเวลาคิดจริง และทีมเถียงกันเรื่องนี้ได้เป็นชั่วโมง - คุณต้องสลับไฟล์ไปมา — อยากรู้ว่าปุ่มนี้หน้าตายังไง ต้องไปเปิดอีกไฟล์ แล้วค้นหาชื่อคลาส
- คุณลบของทิ้งไม่กล้า — จะลบ
.primary-buttonทิ้ง ต้องมั่นใจว่าไม่มีหน้าไหนในโปรเจกต์ใช้อยู่ ผลคือไฟล์ CSS มีแต่โตขึ้น ไม่มีวันเล็กลง
Tailwind ตอบทั้งสามข้อด้วยวิธีเดียว: ไม่ต้องตั้งชื่ออะไรเลย ใช้ชื่อที่มันเตรียมไว้ให้ แล้วชื่อพวกนั้นก็คือชื่อของ CSS property ตรง ๆ
<button className="px-2.5 py-1.5 rounded-md border-2 border-transparent">
กดฉัน
</button>
สองก้อนข้างบนนี้ให้ผลเหมือนกันเป๊ะ ต่างกันแค่ว่าก้อนหลังไม่มีไฟล์ CSS ที่คุณต้องดูแล และไม่มีชื่อที่คุณต้องคิด
การเขียน CSS เองคือการผสมเครื่องเทศเองทุกครั้ง แล้วตักใส่ขวดเปล่า จากนั้นต้องเขียนป้ายชื่อแปะขวด ("พริกเกลือสูตรร้าน") แล้วจำให้ได้ว่าขวดนั้นอยู่ชั้นไหน
Tailwind คือกล่องที่แบ่งช่องเล็ก ๆ ไว้ให้แล้ว ช่องละอย่างเดียว: ช่องเกลือ ช่องพริก ช่องน้ำตาล เวลาทำอาหารคุณไม่ต้องผสมล่วงหน้าและไม่ต้องเขียนป้าย — หยิบทีละช่องใส่ลงหม้อตรงหน้าเลย ใครเดินมาดูหม้อก็รู้ทันทีว่าใส่อะไรไปบ้าง ไม่ต้องไปเปิดขวดอ่านสูตร
ที่ไม่เหมือนกัน: กล่องเครื่องเทศจริงมีช่องจำกัด หมดแล้วหมดเลย
แต่ Tailwind สร้างช่องใหม่ให้ตอนคุณพิมพ์ — พิมพ์ p-4 มันถึงจะมีกฎ .p-4
ถ้าไม่มีไฟล์ไหนพิมพ์คำนั้น กฎนั้นจะไม่ถูกสร้างเลย
ข้อนี้ฟังดูเป็นข้อดี (ไฟล์เล็ก) แต่มันคือต้นตอของกับดักใหญ่ที่สุดในหัวข้อ 6 — จำไว้ก่อน
2 · utility class ไม่ใช่ภาษาใหม่ — มันคือ CSS ที่เขียนไว้ให้แล้ว
นี่คือประโยคสำคัญที่สุดของบทนี้: flex ไม่ใช่คำสั่งวิเศษ มันคือชื่อของกฎ CSS หนึ่งกฎ
ที่หน้าตาแบบนี้เป๊ะ ๆ
.flex {
display: flex;
}
บรรทัดข้างบนนี้ผมไม่ได้พิมพ์เอง — ผมเปิดไฟล์ CSS ที่โปรเจกต์นี้สร้างขึ้นมาจริงแล้วก็อปออกมา ทำแบบเดียวกันกับคลาสที่ใช้บ่อยที่สุด ได้ตารางนี้:
| utility ที่คุณพิมพ์ | CSS ที่ถูกสร้างขึ้นจริง (ก็อปจากไฟล์) | แปลว่าเท่าไหร่ |
|---|---|---|
flex | display: flex | — |
flex-col | flex-direction: column | — |
items-center | align-items: center | — |
p-4 | padding: calc(var(--spacing) * 4) | 1rem = 16px |
p-8 | padding: calc(var(--spacing) * 8) | 2rem = 32px |
px-2.5 | padding-inline: calc(var(--spacing) * 2.5) | 0.625rem = 10px |
py-1.5 | padding-block: calc(var(--spacing) * 1.5) | 0.375rem = 6px |
mt-2 | margin-top: calc(var(--spacing) * 2) | 0.5rem = 8px |
gap-4 | gap: calc(var(--spacing) * 4) | 1rem = 16px |
border | border-style: var(--tw-border-style); border-width: 1px | 1px |
border-2 | border-style: var(--tw-border-style); border-width: 2px | 2px |
border-transparent | border-color: #0000 | โปร่งใส |
rounded-md | border-radius: var(--radius-md) | 0.375rem = 6px |
rounded-lg | border-radius: var(--radius-lg) | 0.5rem = 8px |
text-base | font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)) | 1rem + line-height 1.5 |
text-sm | font-size: var(--text-sm); … | 0.875rem = 14px |
font-bold | --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold) | 700 |
text-red-500 | color: var(--color-red-500) | #fb2c36 |
text-gray-900 | color: var(--color-gray-900) | #101828 |
duration-300 | --tw-duration: .3s; transition-duration: .3s | 0.3 วินาที |
อ่านตารางนี้จบแล้วคุณจะเห็นว่า ไม่มีอะไรใหม่ให้เรียนเลยสักบรรทัด ทุกฝั่งขวาคือ CSS จากบท 59–62 ทั้งหมด · สิ่งเดียวที่ต้องเรียนคือวิธีย่อ
ระวัง: p-4 ไม่ได้แปลว่า padding: 1rem ตรง ๆ
หลายบทความบอกว่า p-4 คือ padding: 1rem ซึ่งผลลัพธ์ถูก แต่กลไกผิด
ของจริงที่ Tailwind 4 เขียนออกมาคือ padding: calc(var(--spacing) * 4)
โดยที่ในไฟล์เดียวกันนั้นประกาศไว้ว่า:
--spacing: .25rem;
เลขหลังขีดจึงเป็นตัวคูณ ไม่ใช่หน่วย → 4 × 0.25rem = 1rem = 16px
ผมลองจริงแล้ว — เขียน @theme { --spacing: 0.2rem; } ทับลงไป
แล้วดึงไฟล์ CSS ที่ถูกสร้างมาดู ได้ --spacing: .2rem; จริง
ส่วนกฎ .p-4 .p-6 .mt-2 ไม่ได้ถูกแก้สักตัว
เพราะมันอ้าง var(--spacing) อยู่แล้ว →
ระยะห่างทั้งเว็บขยับพร้อมกันหมดจากบรรทัดเดียว นี่คือประโยชน์จริงของการเขียนเป็น calc()
แทนที่จะฝังเลข 1rem ลงไปตรง ๆ
ตัวเลขที่ไม่ได้ใช้ --spacing ก็มี เช่นสีกับขนาดตัวอักษร ซึ่งเป็นตัวแปรของมันเอง:
/* ก็อปจากไฟล์ CSS ที่โปรเจกต์สร้างจริง */
--spacing: .25rem;
--text-base: 1rem;
--text-sm: .875rem;
--radius-md: .375rem;
--radius-lg: .5rem;
--font-weight-bold: 700;
--color-red-500: #fb2c36;
--color-gray-900: #101828;
--default-transition-duration: .15s;
- utility class
- คลาสที่ทำหน้าที่เดียว —
.flexตั้งค่าdisplayอย่างเดียว จบ · ตรงข้ามกับ component class อย่าง.primary-buttonที่ตั้งหลายค่าในกฎเดียว · ถาม senior ได้ว่า “อันนี้ควรเป็น utility ล้วน ๆ หรือควรยกเป็น component ไปเลย” คำถามนี้คือเส้นแบ่งที่ทีมต้องตกลงกันตั้งแต่ต้นโปรเจกต์ - utility-first
- คำที่เว็บ Tailwind ใช้เรียกตัวเอง แปลว่าตั้งต้นที่ utility ก่อนเสมอ แล้วค่อยยุบเป็น component ตอนที่ซ้ำจริง ๆ (ไม่ใช่ตอนที่ "รู้สึกว่ายาว") · พิมพ์บอก AI ได้ว่า “style this with Tailwind utility classes, no custom CSS file” จะได้โค้ดที่ตรงกว่าบอกแค่ “ใช้ Tailwind”
- design token
- ค่ากลางที่ทั้งเว็บอ้างถึง —
--spacing--radius-md--color-red-500คือ token ทั้งหมด · ประโยชน์คือแก้ที่เดียว เปลี่ยนทั้งเว็บ · ถาม senior ได้ว่า “ทีมเรามี design token ชุดไหนบ้าง แล้วมันอยู่ไฟล์ไหน” เป็นคำถามแรกที่ควรถามตอนเข้าโปรเจกต์ frontend ใหม่
3 · Tailwind เข้ามาในโปรเจกต์ยังไง — PostCSS กับ import บรรทัดเดียว
Tailwind ไม่ใช่ไฟล์ CSS สำเร็จรูปที่แจกมาแล้วคุณลิงก์เอา มันเป็นโปรแกรมที่รันตอน build แล้วเขียนไฟล์ CSS ขึ้นมาให้ โปรแกรมนั้นเสียบเข้ากับระบบชื่อ PostCSS
ในโปรเจกต์ที่ create-next-app สร้างให้ มันตั้งค่าไว้ให้เรียบร้อยแล้วสองที่ — ไฟล์แรกคือ postcss.config.mjs:
// postcss.config.mjs — ก็อปจากโปรเจกต์จริง ไม่ได้แก้อะไรเลย
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
ไฟล์ที่สองคือ app/globals.css ซึ่งบรรทัดแรกสุดของมันคือทั้งหมดที่ต้องมี:
/* app/globals.css — ก็อปจากโปรเจกต์จริง ตรงกับสไลด์หน้า 27 */
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
/* …ที่เหลือคือ CSS ปกติของคุณ เขียนต่อได้ตามใจ */
บล็อก @theme คือที่เดียวที่ใช้ตั้งค่า Tailwind ใน v4 — มันมาแทน
tailwind.config.js ของ v3 ทั้งไฟล์ · อยากเปลี่ยน --spacing เปลี่ยนสี เพิ่มฟอนต์
ก็เขียนเป็น CSS variable ลงในบล็อกนี้ (คำว่า inline ที่ต่อท้ายเป็นตัวเลือกเสริม
บอกให้ token อ้างตัวแปรอื่นต่อได้ ซึ่งจำเป็นตรงนี้เพราะ --background เปลี่ยนค่าตามโหมดมืด)
แล้ว globals.css ถูก import เข้ามาที่ app/layout.tsx ทีเดียว ครอบทุกหน้า
(นี่คือเหตุผลที่คุณใช้ Tailwind ในหน้าไหนก็ได้โดยไม่ต้อง import ซ้ำ)
คุณพิมพ์ในไฟล์ .tsx Tailwind (ทำงานตอน build) เบราว์เซอร์ได้
───────────────────── ────────────────────────────── ─────────────────
className="p-4 flex" ──┐
className="text-sm" ──┤ 1. อ่าน "ตัวอักษร" ในไฟล์โปรเจกต์
className="rounded-md" ──┘ หาคำที่หน้าตาเหมือนชื่อ utility
│
▼
app/globals.css 2. เห็น @import "tailwindcss"
@import "tailwindcss"; ────► → รู้ว่าต้องสร้าง CSS ตรงนี้
│
▼
postcss.config.mjs 3. PostCSS เรียก @tailwindcss/postcss .p-4 { … }
"@tailwindcss/postcss" ────► ให้แปลงไฟล์ CSS ────► .flex { … }
.text-sm { … }
.rounded-md { … }
▲
สร้าง "เฉพาะคลาสที่เจอ" เท่านั้น ────┘
ขั้นที่ 1 คือขั้นที่คนพลาดกันมากที่สุด และผมจะพิสูจน์ให้ดูในหัวข้อ 6 — Tailwind อ่านโค้ดของคุณในฐานะ "ตัวอักษร" ไม่ใช่ในฐานะ "โปรแกรมที่รันแล้ว" มันไม่รู้ว่าตัวแปรของคุณมีค่าอะไร มันรู้แค่ว่าในไฟล์มีคำว่าอะไรบ้าง
- PostCSS
- เครื่องมือที่รับไฟล์ CSS เข้าไป แล้วคายไฟล์ CSS ที่ถูกแปลงแล้วออกมา
ตัวมันเองไม่ทำอะไรเลย ความสามารถทั้งหมดมาจาก plugin ที่เสียบเข้าไป ·
ถาม senior ได้ว่า “โปรเจกต์เรามี PostCSS plugin อะไรอยู่บ้างนอกจาก Tailwind” —
มักจะเจอ
autoprefixerซ่อนอยู่ในโปรเจกต์เก่า - build step
- ช่วงที่โค้ดของคุณถูกแปลงเป็นไฟล์ที่เบราว์เซอร์อ่านได้ ก่อนที่ผู้ใช้จะเปิดเว็บ · Tailwind ทำงานทั้งหมดในช่วงนี้ ไม่มีโค้ดของ Tailwind วิ่งอยู่ในเบราว์เซอร์เลย · พิมพ์บอก AI ได้ว่า “this class is generated at build time, not at runtime” เมื่อต้องอธิบายว่าทำไมคลาสที่สร้างจากตัวแปรถึงใช้ไม่ได้
- plugin
- โปรแกรมเสริมที่เสียบเข้ากับเครื่องมือหลัก ·
@tailwindcss/postcssคือ plugin ที่ทำให้ PostCSS รู้จักคำว่า@import "tailwindcss"· ถ้าลบบรรทัดนี้ออกจากpostcss.config.mjsจะไม่มี error แต่สไตล์หายทั้งเว็บ — ประโยคนี้ใช้บอก AI ได้ตรง ๆ ตอนดีบั๊ก
4 · เวอร์ชัน — สไลด์กับโปรเจกต์จริงตรงกัน แต่อินเทอร์เน็ตไม่ตรง
ข่าวดี: สไลด์หน้า 25–27 แสดงของ Tailwind v4 แล้ว ไม่ใช่ v3
สไลด์หน้า 26 โชว์ "@tailwindcss/postcss" ใน postcss.config.mjs ·
หน้า 27 โชว์ @import "tailwindcss"; ใน globals.css ·
และหน้า 25 จับภาพเว็บ tailwindcss.com ตอนที่ป้ายเวอร์ชันเขียนว่า v4.3
ทั้งสามอย่างนี้ตรงกับโปรเจกต์จริงบนเครื่องเป๊ะ ๆ (ผมตรวจแล้ว: tailwindcss 4.3.3) — ทำตามสไลด์ได้เลย
แต่บทความ วิดีโอ และคำตอบจาก AI ส่วนใหญ่ยังเป็น v3
v3 ครองอินเทอร์เน็ตอยู่หลายปี เนื้อหาที่ค้นเจอจึงมักเป็นของเก่า ถ้าเจอสองอย่างนี้เมื่อไหร่ แปลว่าคุณกำลังอ่านของ v3 แล้วต้องแปลงก่อนใช้:
| เรื่อง | Tailwind v3 (ของเก่า) | Tailwind v4 — ของจริงในโปรเจกต์นี้ |
|---|---|---|
| เอาเข้าไฟล์ CSS | @tailwind base;@tailwind components;@tailwind utilities; (3 บรรทัด) | @import "tailwindcss"; (บรรทัดเดียว) |
| ไฟล์ตั้งค่า | tailwind.config.js | ไม่มีไฟล์นี้ — ตั้งค่าด้วย @theme ใน CSS |
| PostCSS plugin | tailwindcss + autoprefixer | @tailwindcss/postcss ตัวเดียว |
| บอกว่าสแกนไฟล์ไหน | ตั้ง content: [...] ใน config | หาให้เองอัตโนมัติ ไม่ต้องตั้ง |
| ปรับ token | แก้ theme.extend ใน JS | เขียน CSS variable ใน @theme { } |
สี red-500 | #ef4444 | #fb2c36 (v4 เปลี่ยนไปใช้ระบบสีใหม่) |
ผมลองรันในโปรเจกต์จริงแล้ว ว่า ls tailwind.config.* ไม่เจออะไรเลย —
no matches found · ไฟล์นั้นไม่มีจริง อย่าไปหา
ถ้าเผลอเอาไวยากรณ์ v3 มาใช้ในโปรเจกต์ v4 จะเกิดอะไรขึ้น
ผมสร้างไฟล์ old.css ที่ใส่ไวยากรณ์ v3 ล้วน ๆ แล้ว import เข้าหน้าหนึ่ง แล้วเปิดหน้านั้นดู:
/* app/t72/v3style/old.css — ไวยากรณ์ v3 ในโปรเจกต์ v4 */
@tailwind base;
@tailwind components;
@tailwind utilities;
ผลจริงที่ได้ ตรงข้ามกับที่หลายคนคาด:
| สิ่งที่วัด | ผลจริง |
|---|---|
| มี error ตอน compile ไหม | ไม่มี — dev server ไม่บ่นสักบรรทัด |
| หน้าเว็บเปิดได้ไหม | ได้ — HTTP 200 |
| ได้ไฟล์ CSS ออกมาไหม | ได้ แต่ผอมผิดปกติ — 4,089 bytes ขณะที่หน้าปกติในโปรเจกต์เดียวกันได้ 21,648 bytes |
มี .flex ไหม | ✅ มี |
มี .p-4 ไหม | ❌ ไม่มี |
มี --spacing ไหม | ❌ ไม่มี (0 ครั้ง) |
มี preflight (box-sizing reset) ไหม | ❌ ไม่มี (0 ครั้ง) |
ผลนี้แย่กว่าพังไปเลย — ถ้ามันขึ้น error คุณจะรู้ทันทีว่าต้องแก้ตรงไหน
แต่มันดัน "ทำงานได้ครึ่งเดียว": คลาสที่ไม่ต้องพึ่ง token อย่าง .flex ยังมา
ส่วนคลาสที่ต้องคำนวณจาก --spacing อย่าง .p-4 หายไปเงียบ ๆ
คุณจะนั่งงงว่า "ทำไม flex ติดแต่ padding ไม่ติด" อยู่ครึ่งชั่วโมง
บทเรียน: ถ้าเจอหน้าที่สไตล์มาบ้างไม่มาบ้าง ให้เช็คไวยากรณ์เวอร์ชันก่อนเช็คอย่างอื่น
5 · เอาไปใช้จริง — ใส่สไตล์ให้ Button ของบท 71
สไลด์หน้า 28 เอา Button จากบท 71 มาใส่คลาส Tailwind ผมพิมพ์ตามสไลด์แล้วรันจริง ได้แบบนี้:
// components/Bases/Button.tsx
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = (props: ButtonProps) => {
const { children, ...restProps } = props;
return (
<button
className={`px-2.5 py-1.5
text-base text-[#aa3bff] bg-[rgba(170,59,255,0.1)]
border-2 hover:border-[rgba(170,59,255,0.5)] border-transparent rounded-md
transition-colors duration-300`}
{...restProps}
>
{children}
</button>
);
};
export default Button;
เรียกใช้แบบเดิมเป๊ะ — คนใช้ไม่ต้องรู้เลยว่าข้างในเป็น Tailwind:
<Button
type="button"
onClick={() => console.log("CLICKED !")}
>
Test Button
</Button>
HTML ที่เซิร์ฟเวอร์ส่งออกมาจริง (ดึงด้วย curl):
<button class="px-2.5 py-1.5
text-base text-[#aa3bff] bg-[rgba(170,59,255,0.1)]
border-2 hover:border-[rgba(170,59,255,0.5)] border-transparent rounded-md
transition-colors duration-300" type="button">Test Button</button>
สองอย่างในโค้ดนี้ที่ต้องอธิบาย
อย่างแรก — วงเล็บเหลี่ยม [...] คือ "ค่าที่ฉันขอกำหนดเอง"
Tailwind ไม่มีสีชื่อ #aa3bff เตรียมไว้ให้ คุณจึงยัดค่าดิบเข้าไปในวงเล็บเหลี่ยม
แล้วมันสร้างกฎให้ตอน build เลย ของจริงในไฟล์ CSS ออกมาแบบนี้:
.text-\[\#aa3bff\] {
color: #aa3bff;
}
.bg-\[rgba\(170\,59\,255\,0\.1\)\] {
background-color: #aa3bff1a;
}
สังเกตว่ามันแปลง rgba(170,59,255,0.1) เป็น #aa3bff1a ให้เอง (ค่าเดียวกัน เขียนสั้นกว่า)
· ส่วน \ เต็มไปหมดคือการ escape เพราะ # [ ( เป็นอักขระพิเศษใน CSS selector
อย่างที่สอง — ลำดับคลาสใน className ไม่มีความหมายเลย
ในโค้ดข้างบน hover:border-[rgba(170,59,255,0.5)] ถูกเขียนก่อน border-transparent
ถ้าคิดแบบ CSS ปกติ คุณอาจกลัวว่า border-transparent จะมาทับตอน hover
แต่ไม่เกิดขึ้น เพราะสิ่งที่ตัดสินคือลำดับในไฟล์ CSS ไม่ใช่ลำดับที่คุณพิมพ์ใน HTML — ผมไปดูไฟล์จริงมา:
/* บรรทัดที่ 614 ของไฟล์ CSS ที่ผมดึงมา */
.border-transparent {
border-color: #0000;
}
/* บรรทัดที่ 824–827 — มาทีหลังตั้ง 200 บรรทัด และมี :hover เพิ่ม specificity ด้วย */
@media (hover: hover) {
.hover\:border-\[rgba\(170\,59\,255\,0\.5\)\]:hover {
border-color: #aa3bff80;
}
}
เลขบรรทัดในไฟล์ของคุณจะไม่ตรงกับของผม เพราะมันขึ้นกับว่าโปรเจกต์คุณใช้คลาสอะไรบ้าง · สิ่งที่ต้องดูคือ "อันไหนอยู่ก่อน อันไหนอยู่หลัง" ไม่ใช่ตัวเลข
Tailwind เรียงลำดับกฎให้เองตอน build โดยเอา variant อย่าง hover: ไว้ท้ายเสมอ
คุณจึงพิมพ์คลาสสลับกันยังไงก็ได้ ผลเหมือนเดิม
โบนัสที่เจอตอนอ่านไฟล์: Tailwind 4 ห่อ hover: ไว้ใน @media (hover: hover)
แปลว่าบนมือถือที่ไม่มีเมาส์ สไตล์ hover จะไม่ทำงานเลย — ตั้งใจให้เป็นแบบนั้น
เพราะบนจอสัมผัส hover มัก "ค้าง" อยู่หลังผู้ใช้แตะแล้วปล่อย ·
อย่าเอาข้อมูลสำคัญไปซ่อนไว้ใต้ hover: เพราะคนใช้มือถือจะไม่เห็นเลย
className="px-2.5 py-1.5 rounded-md" เหมือนบอกว่าวันนี้ใส่อะไรบ้าง
(เสื้อยืดขาว กางเกงยีนส์ รองเท้าผ้าใบ) — บอกทีละชิ้น เห็นครบทันทีว่ามีอะไร
ส่วน <Button> ที่ห่อคลาสพวกนั้นไว้ข้างใน เหมือนชุดยูนิฟอร์มที่แขวนพร้อมในตู้
หยิบมาใส่ทั้งชุด ไม่ต้องเลือกทีละชิ้น และทุกคนในทีมได้ชุดเดียวกัน
ที่ไม่เหมือนกัน: ยูนิฟอร์มจริงเปลี่ยนได้ยาก ต้องสั่งตัดใหม่ทั้งล็อต
แต่ Button คุณแก้ที่ไฟล์เดียว แล้วทุกปุ่มในเว็บเปลี่ยนพร้อมกันทันที
— นี่คือเหตุผลทั้งหมดที่ต้องมี component ครอบ ไม่ใช่พิมพ์คลาสยาว ๆ ซ้ำทุกที่
· แต่ก็แปลว่าคุณทำพังทั้งเว็บได้ในบรรทัดเดียวเหมือนกัน
6 · กับดักที่ทำให้สไตล์หายเงียบ ๆ — ชื่อคลาสที่สร้างจากตัวแปร
นี่คือบั๊ก Tailwind อันดับหนึ่งที่คนใหม่เจอ และมันไม่มี error ให้เห็นแม้แต่ตัวเดียว
ผมเขียนหน้านี้แล้วรันจริงบน dev server ลองทายก่อนกดเฉลย:
// app/t72/dynamic/page.tsx
export default function Page() {
const color = "blue";
const shade = 600;
return (
<div className="p-4">
<p className={`text-${color}-${shade}`}>สร้างชื่อคลาสด้วยการต่อสตริง</p>
<p className="text-green-700">เขียนชื่อคลาสเต็ม ๆ ตรง ๆ</p>
</div>
);
}
เปิดหน้านี้ในเบราว์เซอร์ บรรทัดไหนจะเป็นสีน้ำเงิน บรรทัดไหนจะเป็นสีเขียว?
แล้วในไฟล์ CSS ที่ถูกสร้างขึ้น จะมีกฎ .text-blue-600 กี่กฎ?
บรรทัดแรกเป็นสีดำ (สีปกติ) · บรรทัดที่สองเป็นสีเขียว
และในไฟล์ CSS มีกฎ .text-blue-600 ศูนย์กฎ
ผลจริงที่วัดได้:
# HTML ที่ส่งออกมา — คลาสอยู่ครบนะ
class="text-blue-600"
class="text-green-700"
# แต่ในไฟล์ CSS ที่ Tailwind สร้าง นับจำนวนครั้งที่เจอคำนั้น
text-blue-600 -> 0
text-green-700 -> 1
# กฎของ text-green-700 ที่มีอยู่จริง
.text-green-700 { color: var(--color-green-700); }
# โดยที่ --color-green-700: #008138;
ทำไม — ย้อนไปที่ขั้นที่ 1 ของแผนภาพในหัวข้อ 3:
Tailwind อ่านไฟล์ของคุณเป็นตัวอักษร ตอน build
ตอนนั้นโปรแกรมของคุณยังไม่ได้รัน ตัวแปร color จึงยังไม่มีค่า
สิ่งที่ Tailwind เห็นในไฟล์คือข้อความตรง ๆ ว่า text-${color}-${shade}
ซึ่งไม่ใช่ชื่อ utility ที่มันรู้จัก → มันเลยไม่สร้างกฎอะไรเลย
พอถึงตอนที่โปรแกรมรันจริงในเบราว์เซอร์ React ก็ใส่ class="text-blue-600" ให้อย่างซื่อสัตย์
แต่ตอนนั้นสายเกินไปแล้ว ไฟล์ CSS ถูกสร้างเสร็จตั้งแต่ตอน build
· ไม่มี error ไม่มี warning ใน terminal สักบรรทัด · หน้าเว็บตอบ HTTP 200 ปกติทุกอย่าง
วิธีแก้: เขียนชื่อคลาสให้ครบทุกตัวไว้ในโค้ด แล้วค่อยเลือกทีหลัง
❌ สร้างชื่อจากตัวแปร — สไตล์หาย
const color = "blue";
<p className={`text-${color}-600`}>
Tailwind ไม่เคยเห็นคำว่า text-blue-600 ในไฟล์ไหนเลย จึงไม่สร้างกฎให้
✅ เขียนชื่อเต็มไว้ทั้งหมด แล้วเลือก
const colors = {
blue: "text-blue-600",
green: "text-green-700",
};
<p className={colors[color]}>
ชื่อเต็มปรากฏเป็นตัวอักษรในไฟล์แล้ว Tailwind สแกนเจอ จึงสร้างกฎให้ทั้งสองอัน
- arbitrary value
- ค่าที่คุณกำหนดเองในวงเล็บเหลี่ยม เช่น
text-[#aa3bff]w-[158px]· ใช้ได้ แต่ทุกครั้งที่ใช้แปลว่าคุณกำลังหลุดออกจาก design token ของทีม · ถาม senior ได้ว่า “ค่านี้ควรเป็น arbitrary value หรือควรเพิ่มเข้า theme ไปเลย” ถ้าค่าเดิมโผล่เกิน 2–3 ที่ คำตอบมักเป็นอย่างหลัง - variant (prefix)
- คำนำหน้าที่ตามด้วย
:ซึ่งบอกว่า "ใช้กฎนี้เฉพาะตอนไหน" —hover:sm:dark:focus:· มันจะกลายเป็น pseudo-class หรือ media query ในไฟล์ CSS · พิมพ์บอก AI ได้ว่า “add a dark: variant for these classes” จะตรงกว่าบอกว่า “ทำโหมดมืด” - content scanning
- ขั้นที่ Tailwind ไล่อ่านไฟล์ในโปรเจกต์เพื่อหาว่ามีชื่อ utility อะไรถูกพิมพ์ไว้บ้าง แล้วสร้างเฉพาะอันนั้น — นี่คือเหตุผลที่ไฟล์ CSS เล็ก และเป็นเหตุผลที่ชื่อคลาสจากตัวแปรใช้ไม่ได้ · ถาม senior ได้ว่า “คลาสนี้ Tailwind สแกนเจอไหม” — เป็นคำถามที่ตัดปัญหา "สไตล์ไม่ติด" ได้เร็วที่สุด
7 · ทวนความเข้าใจ
ในโปรเจกต์ Tailwind v4 นี้ ถ้าอยากเปลี่ยนค่าฐานของระยะห่างทั้งเว็บ ต้องไปแก้ที่ไหน
ตัวเลือกที่ 2 ถูก — v4 ย้ายการตั้งค่ามาอยู่ใน CSS ทั้งหมด ผ่านบล็อก @theme
ซึ่งประกาศ token อย่าง --spacing --radius-md --color-red-500
ตัวเลือกที่ 1 คือวิธีของ v3 · ผมรัน ls tailwind.config.* ในโปรเจกต์จริงแล้ว
ได้ no matches found — ไฟล์นั้นไม่มีอยู่
ตัวเลือกที่ 3 คือที่ที่บอกว่า "ให้ PostCSS เรียก Tailwind" ไม่ได้เกี่ยวกับค่าระยะห่าง ·
ตัวเลือกที่ 4 คือ content ของ v3 ซึ่ง v4 หาไฟล์ให้เองแล้ว
จำง่าย ๆ: v4 = ตั้งค่าใน CSS · v3 = ตั้งค่าใน JS
คลาส p-4 ถูกแปลงเป็น CSS อะไร ในโปรเจกต์นี้
ตัวเลือกที่ 4 ถูก — ผมก็อปมาจากไฟล์ CSS ที่โปรเจกต์สร้างจริง มันเขียนไว้อย่างนั้นเป๊ะ ๆ
ในไฟล์เดียวกันประกาศว่า --spacing: .25rem ผลสุดท้ายจึงเท่ากับ
4 × 0.25rem = 1rem = 16px
ตัวเลือกที่ 1 และ 2 ให้ค่าผิด (4px เล็กเกินไป · 4rem = 64px ใหญ่เกินไป) และที่สำคัญกว่าคือ มันเข้าใจกลไกผิด — เลขหลังขีดเป็นตัวคูณ ไม่ใช่หน่วย
ตัวเลือกที่ 3 ฟังดูสมเหตุสมผลแต่ไม่มีตัวแปรชื่อ --p-4 อยู่จริงในไฟล์ ·
ข้อนี้สำคัญเพราะเข้าใจถูกแล้วคุณจะรู้ว่าทำไมแก้ค่าเดียว ระยะห่างทั้งเว็บถึงขยับตาม
โค้ด <p className={`text-${color}-600`}> โดยที่ color = "blue" จะเกิดอะไรขึ้น
ตัวเลือกที่ 1 ถูก — ผมรันจริงแล้ววัดได้ว่า HTML มี class="text-blue-600"
แต่ในไฟล์ CSS นับคำว่า text-blue-600 ได้ 0 ครั้ง
ขณะที่ text-green-700 ที่เขียนเต็มไว้ นับได้ 1 ครั้ง
ตัวเลือกที่ 2 ผิดเพราะ Tailwind ไม่ได้รันโปรแกรมของคุณ มันแค่อ่านไฟล์เป็นตัวอักษร
ตอนสแกน ตัวแปร color ยังไม่มีค่าใด ๆ ทั้งสิ้น
ตัวเลือกที่ 3 และ 4 คือสิ่งที่เราอยากให้เกิด แต่ไม่เกิด —
ผมรันแล้ว dev server เงียบสนิท ไม่มี error ไม่มี warning และหน้าเว็บตอบ HTTP 200
นี่คือเหตุผลที่บั๊กนี้กินเวลาคนเยอะมาก: มันไม่ส่งเสียงเลย
ในโค้ดของ Button หัวข้อ 5 — className="border-2 hover:border-[rgba(170,59,255,0.5)] border-transparent"
— ตอนเอาเมาส์ไปชี้ ขอบจะเป็นสีอะไร
ตัวเลือกที่ 3 ถูก — class ใน HTML เป็นแค่รายชื่อ ไม่ใช่ลำดับความสำคัญ
เบราว์เซอร์ตัดสินจากไฟล์ CSS ล้วน ๆ
ผมเปิดไฟล์ CSS จริงมาดู: .border-transparent อยู่บรรทัด 614
ส่วน .hover\:border-[…]:hover อยู่บรรทัด 825 · hover มาทีหลัง
และยังมี specificity สูงกว่าด้วยเพราะมี :hover ต่อท้าย จึงชนะสองต่อ
(เลขบรรทัดของคุณจะต่างไป ดูแค่ว่าอันไหนมาก่อน)
ตัวเลือกที่ 1 และ 2 คือความเข้าใจผิดที่พบบ่อยว่า "คลาสหลังทับคลาสหน้า" — ไม่จริงเลยใน CSS
ตัวเลือกที่ 4 ใกล้เคียงแต่ผิดจุดสำคัญ: Tailwind เรียงลำดับกฎในไฟล์ CSS
(เอา variant ไว้ท้าย) ไม่ได้ไปแตะสตริงใน className ของคุณเลย —
HTML ที่ curl ได้ ยังเรียงตามที่พิมพ์ทุกตัว
8 · ภารกิจ — พิสูจน์ด้วยไฟล์ CSS จริง ไม่ใช่ด้วยสายตา
ภารกิจ: จับ Tailwind ให้ได้คาหนังคาเขา
ทำในโปรเจกต์ Next.js ของคุณเอง · ห้ามตัดสินด้วยการมองหน้าเว็บอย่างเดียว เพราะกับดักในบทนี้ทั้งหมด "มองแล้วดูปกติ"
- สร้าง
app/tw-lab/page.tsxใส่กล่องหนึ่งกล่องด้วยคลาสp-6 rounded-lg border-2 border-gray-300 text-lgแล้วnpm run dev - เปิด DevTools → แท็บ Elements → เลือกกล่องนั้น → ดูช่อง Styles ฝั่งขวา
แล้วจดลงกระดาษว่าแต่ละคลาสกลายเป็น CSS อะไร
(คำใบ้:
p-6จะขึ้นเป็นcalc(var(--spacing) * 6)ไม่ใช่1.5rem) - ในช่อง Computed ของ DevTools ดูค่าจริงเป็น px ของ
paddingแล้วเช็คว่าตรงกับที่คุณคำนวณเองจาก--spacing: .25remไหม - เขียนเทสพิสูจน์กับดัก — เพิ่มสองบรรทัดนี้ในหน้าเดียวกัน:
แล้วเปิดไฟล์ CSS ที่หน้านั้นโหลด (DevTools → Network → กรอง CSS → คลิกไฟล์ → Response) กด Ctrl/Cmd+F ค้นคำว่าconst tone = "rose"; <p className={`text-${tone}-600`}>ไดนามิก</p> <p className="text-rose-600">เขียนเต็ม</p>text-rose-600
เกณฑ์ผ่าน: คุณต้องตอบได้ว่าเจอกี่ครั้ง และอธิบายได้ว่าทำไมไม่ใช่สองครั้ง - เทสข้อสุดท้าย — เอาคลาสใดคลาสหนึ่งที่คุณไม่ได้ใช้ที่ไหนเลย เช่น
text-9xlแล้วค้นหาในไฟล์ CSS เดียวกันนั้น · ควรได้ 0 ครั้ง
จากนั้นไปพิมพ์text-9xlใส่ที่ไหนสักที่ในโปรเจกต์ เซฟ แล้วค้นใหม่
เกณฑ์ผ่าน: เห็นตัวเลขเปลี่ยนจาก 0 เป็น 1 ด้วยตาตัวเอง — ตรงนี้แหละคือ content scanning ที่คุณเพิ่งเรียนไป
ข้อ 2–3: p-6 → padding: calc(var(--spacing) * 6) →
6 × 0.25rem = 1.5rem = 24px ในช่อง Computed ·
rounded-lg → border-radius: var(--radius-lg) → 0.5rem = 8px ·
border-2 → border-width: 2px
ข้อ 4 คำตอบคือ 1 ครั้ง — มาจากบรรทัดที่เขียนชื่อเต็มเท่านั้น
ส่วน `text-${tone}-600` ไม่ได้ทำให้เกิดกฎอะไรเลย เพราะตอน Tailwind สแกนไฟล์
มันเห็นแค่ตัวอักษร text-${tone}-600 ซึ่งไม่ใช่ชื่อ utility ที่มันรู้จัก
ที่หลอกตาคือสองบรรทัดนี้จะดูเหมือนกันบนหน้าเว็บถ้าคุณบังเอิญเลือกสีที่ใกล้สีตัวอักษรปกติ —
เหตุผลที่ภารกิจนี้บังคับให้เปิดไฟล์ CSS ดู ไม่ใช่ให้มองเอา
ข้อ 5: ถ้าตัวเลขไม่เปลี่ยนจาก 0 เป็น 1 ให้ hard refresh
(Cmd/Ctrl+Shift+R) เพราะเบราว์เซอร์ชอบ cache ไฟล์ CSS ไว้ ·
ผมเจอเรื่องนี้ตอนตรวจบทนี้เหมือนกัน — ครั้งแรกที่ดึงไฟล์มาได้ของเก่า
เลยเห็น text-orange-600 เป็น 0 ทั้งที่มันควรมี พอดึงใหม่แบบ no-cache ถึงได้ 1
ติดตรงไหนถามได้เลย — ถ้าสไตล์ไม่ติดแล้วหาไม่เจอว่าทำไม
ส่งมาสามอย่างนี้: (1) บรรทัด className ที่มีปัญหา
(2) ผลค้นหาชื่อคลาสนั้นในไฟล์ CSS ว่าเจอกี่ครั้ง (3) ไวยากรณ์ใน globals.css บรรทัดแรก
สามอย่างนี้ตัดสาเหตุได้เกือบทั้งหมดของบทนี้แล้ว
อ้างอิง
- Tailwind CSS — Styling with utility classes —
อ่านหัวข้อ Why not just use inline styles? ซึ่งตอบคำถามที่คุณน่าจะกำลังคิดอยู่ว่า
"ถ้าจะยัดสไตล์ใส่ tag แบบนี้ ใช้
style=ไปเลยไม่ดีกว่าเหรอ" · คำตอบสั้น ๆ คือstyle=ทำhover:กับsm:ไม่ได้ และไม่ผูกกับ design token - Tailwind CSS — Detecting classes in source files — อ่านหัวข้อ Dynamic class names ซึ่งเป็นเอกสารทางการของกับดักในหัวข้อ 6 ของบทนี้เป๊ะ ๆ เขาเขียนไว้ตรง ๆ ว่าชื่อคลาสต้องปรากฏเป็นสตริงเต็มในไฟล์ ไม่งั้นไม่ถูกสร้าง
- Tailwind CSS — Theme variables —
อ่านตาราง Theme variable namespaces เพื่อดูว่า
--spacing--color-*--text-*--radius-*แต่ละกลุ่มไปควบคุม utility ตัวไหนบ้าง · นี่คือแผนที่ที่ใช้แทนtailwind.config.jsของ v3 - Tailwind CSS — Upgrade guide (v3 → v4) — อ่านหัวข้อ Changes from v3 ตอนที่คุณค้นเจอบทความเก่าแล้วไม่แน่ใจว่าต้องแปลงอะไรบ้าง · โดยเฉพาะย่อหน้าเรื่อง CSS-first configuration ที่อธิบายว่าทำไมไฟล์ config ถึงหายไป
- Next.js — CSS —
อ่านหัวข้อ Global CSS ให้เห็นว่าทำไม
globals.cssต้องถูก import ที่app/layout.tsxไฟล์เดียว แล้วมันครอบทุกหน้าได้ยังไง - PostCSS — อ่านแค่ย่อหน้าแรกของหน้าแรกก็พอ ให้เห็นภาพว่ามันคือ "เครื่องมือแปลง CSS ด้วย JavaScript" แล้ว Tailwind เป็นเพียง plugin ตัวหนึ่งของมัน — ไม่ต้องลงลึกกว่านี้ในตอนนี้