Tailwind Setup and Utilities
Day 3 - ชั่วโมงที่ 1: เปลี่ยนจาก Custom CSS มาใช้ Tailwind
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงนี้ ผู้เรียนควรสามารถ:
- อธิบายได้ว่า Tailwind CSS ช่วยเขียน Style อย่างไร
- ตรวจและเปิดใช้งาน Tailwind ใน Next.js Project ได้
- อ่าน Utility Class พื้นฐานได้โดยเชื่อมโยงกับ CSS ที่เคยเรียน
- แปลงโครงหน้าและ Form จาก Custom CSS เป็น Tailwind ทีละส่วนได้
- ลบ CSS เดิมเฉพาะส่วนที่ย้ายเสร็จแล้วได้อย่างปลอดภัย
ไฟล์ที่ใช้
package.json
postcss.config.mjs
app/globals.css
app/layout.tsx
app/page.tsx
components/IssueForm.tsxSlide 1: ต่อจากสิ่งที่สร้างใน Day 2
หลังจบ Day 2 Project ของเรามี:
- ส่วนของหน้าจอที่แยกใช้ซ้ำได้ (Component):
IssueForm,IssueListและStatusBadge - Type ที่กำหนดรูปแบบข้อมูล และข้อมูลตัวอย่าง (Mock Data) ที่แยกเป็นไฟล์กลาง
- หน้า
/,/issues,/issues/newและ/issues/[id] - เมนูนำทางที่ใช้ร่วมกันทุกหน้าผ่าน
app/layout.tsx - CSS ที่เขียนเอง (Custom CSS) ทั้งหมดอยู่ใน
app/globals.css
Project ทำงานได้แล้ว แต่เมื่อ Component เพิ่มขึ้น CSS กลางจะยาวขึ้นและต้องสลับไฟล์ไปมาระหว่าง TSX กับ CSS
ชั่วโมงนี้จะเริ่มใช้ Tailwind กำหนดหน้าตาเว็บใกล้กับโค้ดของแต่ละส่วน โดยยังไม่เปลี่ยนข้อมูลหรือการทำงานของระบบ
Slide 2: จาก Custom CSS ไปสู่ Tailwind
ตัวอย่างเพื่อทำความเข้าใจ: ยังไม่ต้องแก้ Project ใน Slide นี้
ก่อนหน้านี้เราเขียน Style โดยตั้งชื่อ Class แล้วประกาศ CSS แยกอีกไฟล์:
<button className="primary-button">ส่งข้อมูล</button>.primary-button {
padding: 12px 16px;
background: #0f766e;
color: white;
}Tailwind มีคลาสสำเร็จรูป (Utility Class) แต่ละตัวกำหนดค่า CSS เช่น สีหรือระยะห่าง:
<button className="bg-teal-700 px-4 py-3 text-white">
ส่งข้อมูล
</button>Slide 3: ตรวจว่า Tailwind พร้อมใช้งาน
Day 2 เลือก Recommended Defaults จึงมี Tailwind v4 มาแล้ว ไม่ต้องติดตั้งซ้ำ
ตรวจใน package.json ว่ามี:
"tailwindcss": "^4",
"@tailwindcss/postcss": "^4"postcss.config.mjs ต้องเปิดส่วนเสริม (Plugin) นี้:
const config = {
plugins: { "@tailwindcss/postcss": {} },
};
export default config;@tailwindcss/postcss ทำหน้าที่ประมวลผล Tailwind Class ให้กลายเป็น CSS ที่ Browser ใช้งานได้
Slide 4: เปิด Tailwind โดยเก็บ CSS เดิมไว้
@import "tailwindcss"; @layer base { /* ย้าย CSS เดิมทั้งหมดมาไว้ตรงนี้ รวมถึง @media */}
ข้อความใน /* ... */ คือคำอธิบาย (Comment) ที่บอกจุดวาง CSS เดิม ให้เก็บทุกกฎตั้งแต่ * จนถึงเมนูนำทางไว้ภายในวงเล็บนี้ ไม่คัดลอกตัวอย่างทับทั้งไฟล์
@layer base จัด CSS เดิมเป็นชั้นพื้นฐาน เพื่อให้ Utility ของ Tailwind เปลี่ยนค่าทับได้ระหว่างทยอยย้ายแต่ละส่วน
app/layout.tsx ยังต้องมี import "./globals.css"; ตาม Day 2 จึงจะโหลด Style ให้ทุกหน้า
Slide 5: ใช้ Tailwind ตอนไหนและไฟล์ไหน
ตัวอย่างการเขียน Utility ใน className ของไฟล์ .tsx (ยังไม่ต้องแก้):
<h1 className="text-3xl font-bold text-slate-950">
ระบบแจ้งปัญหา IT
</h1>| ไฟล์ | ส่วนที่จะปรับ |
|---|---|
app/layout.tsx | เมนูนำทางที่ทุกหน้าใช้ร่วมกัน |
app/page.tsx | ส่วนหัว เนื้อหาหลัก และส่วนท้ายของหน้า Home |
components/IssueForm.tsx | โครงฟอร์ม ช่องกรอก และปุ่ม |
ไม่ต้อง Import Tailwind ในแต่ละไฟล์ TSX เพราะ app/layout.tsx โหลด globals.css ให้ทุกหน้าแล้ว ส่วน IssueList และ StatusBadge จะเริ่มใช้ Tailwind ใน Hour 2
Slide 6: ย้าย Style ทีละส่วน
globals.css ยังโหลด Tailwind และเก็บ CSS ของส่วนที่ยังไม่ได้ย้าย
- เพิ่ม Utility ใน
classNameของ Tag เดิม - เปิดหน้าเว็บเพื่อดูผล โดย CSS เดิมอยู่ใน
@layer base - เมื่อย้ายครบส่วนแล้ว ค่อยลบกฎที่ไม่ใช้ในช่วงท้ายชั่วโมง
เก็บ main และ section ไว้ก่อน เพราะหลายหน้าและ IssueList ยังใช้รูปแบบกลางเหล่านี้
Tailwind มี Preflight สำหรับปรับ Style เริ่มต้นของ Browser ให้ใกล้เคียงกัน เช่น หัวข้อจะไม่หนาหรือใหญ่โดยอัตโนมัติ จึงต้องกำหนด Utility ให้หัวข้อที่ย้ายแล้ว
Slide 7: Utility สำหรับระยะห่าง
ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องแก้ Project
ตัวอักษรด้านหน้า Class บอกทิศทาง ส่วนตัวเลขบอกระดับของระยะห่าง:
<div className="p-4">Padding ทุกด้าน</div>
<div className="px-6 py-8">Padding แนวนอนและแนวตั้ง</div>
<section className="mt-6">Margin ด้านบน</section>
<div className="grid gap-6">ระยะห่างระหว่างลูก</div>pคือระยะด้านในกล่อง (Padding) ทุกด้านpxคือระยะด้านในซ้าย–ขวา ส่วนpyคือบน–ล่างmคือระยะด้านนอกกล่อง (Margin)gapคือช่องว่างระหว่างส่วนย่อยที่จัดด้วย Flex หรือ Grid
ค่าปกติใช้หน่วยละ 0.25rem เช่น p-4 = 1rem และ p-6 = 1.5rem โดย rem อิงขนาดตัวอักษรของหน้า ไม่ใช่จำนวนพิกเซล
Slide 8: Utility สำหรับข้อความและกล่อง
ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องนำ Code ชุดนี้ไปวางใน Project
<section className="rounded-lg border border-slate-200 bg-white p-6">
<h2 className="text-xl font-bold text-slate-950">
รายการปัญหา
</h2>
<p className="mt-2 text-sm text-slate-600">
รายการปัญหาที่ถูกแจ้งเข้ามาในระบบ
</p>
</section>text-xl,text-smคือขนาด ส่วนfont-boldคือความหนาtext-slate-*คือสีข้อความ ส่วนbg-whiteคือสีพื้นborder border-slate-200คือเส้นขอบสีเทาอ่อนrounded-lgทำให้มุมกล่องโค้ง
Slide 9: Utility สำหรับจัดวางเนื้อหา
ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องแก้ Project
จำกัดความกว้างและจัดกลาง
<main className="mx-auto max-w-5xl px-6 py-8">
...
</main>max-w-5xl ป้องกันเนื้อหากว้างเต็มจอบน Desktop ส่วน mx-auto จัดกล่องให้อยู่กลางหน้า
จัดลูกด้วย Flex
<div className="flex items-center justify-between gap-4">
...
</div>Flex จัดวางลูกในแนวเดียว เช่น เมนู ส่วน Grid ใช้จัดหลายคอลัมน์
Slide 10: ปรับหน้าตาตามขนาดจอ
ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องแก้ Project
Tailwind ใช้ Mobile-first คือเริ่มจัดหน้าจอเล็ก แล้วใส่เงื่อนไขเพิ่มสำหรับจอกว้าง:
- คลาสที่ไม่มีคำหน้าคั่นด้วย
:ทำงานกับทุกขนาดจอ sm:เริ่มที่640pxขึ้นไปmd:เริ่มที่768pxขึ้นไปlg:เริ่มที่1024pxขึ้นไป
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
...
</div>ค่าเริ่มต้นคือ 1 คอลัมน์ เมื่อกว้างถึง 768px จึงเปลี่ยนเป็น 2 คอลัมน์
คำหน้าอย่าง md: เรียกว่า Prefix ส่วนความกว้างที่เริ่มเปลี่ยนเรียกว่า Breakpoint; sm: จึงไม่ใช่ “เฉพาะมือถือ”
Slide 11: เปลี่ยนหน้าตาเมื่อผู้ใช้โต้ตอบ
ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องแก้ Project
คำหน้าอย่าง hover: และ focus: ให้คลาสทำงานเฉพาะเมื่อชี้เมาส์หรือเลือกช่องกรอก:
<button className="bg-teal-700 hover:bg-teal-800">
ส่งข้อมูล
</button><input className="border focus:border-teal-700 focus:outline-none focus:ring-4 focus:ring-teal-100" />hover:ทำงานเมื่อผู้ใช้ชี้เมาส์focus:ทำงานเมื่อช่องกรอกหรือปุ่มถูกเลือก เช่น คลิกหรือกด Tab มาถึงdisabled:ทำงานเมื่อช่องกรอกหรือปุ่มถูกปิดการใช้งาน
สีหรือเส้นรอบที่เปลี่ยนไปช่วยบอกว่าผู้ใช้กำลังโต้ตอบกับส่วนใด
Slide 12: จัดกรอบเมนูนำทาง
<nav className="border-b border-slate-200 bg-white" aria-label="เมนูหลัก"> <div className="mx-auto flex max-w-5xl flex-wrap gap-2 px-6 py-3"> {/* เก็บ Link ทั้งสามรายการเดิมไว้ตรงนี้ */} </div></nav>
Comment แสดงตำแหน่งของ Link เดิม ไม่ต้องคัดลอก Comment ไปแทนเมนู
flex-wrap ทำให้เมนูขึ้นบรรทัดใหม่เมื่อพื้นที่ไม่พอ ขั้นถัดไปจะใส่ Style ให้ Link แต่ละรายการ
Slide 13: ตกแต่งลิงก์ในเมนูนำทาง
ตัวอย่าง Link หน้าแรก ใช้ Class ชุดเดียวกันกับ Link /issues และ /issues/new:
<Link className="rounded-md px-3 py-2 font-semibold text-slate-700 hover:bg-slate-100" href="/"> หน้าแรก</Link>
ตัวอย่างเริ่มที่ Link เมื่อวางในไฟล์จริงให้เยื้องอยู่ภายใน <div> ของ Navigation
เมนูควรแสดงครบทุกหน้า และพื้นหลังเปลี่ยนเมื่อชี้เมาส์; เก็บ CSS เดิมจนถึงขั้นลบ Style ท้ายชั่วโมง
Slide 14: แปลง Header เป็น Tailwind
<header className="bg-teal-800 px-6 py-8 text-white"> <div className="mx-auto max-w-5xl"> <h1 className="text-3xl font-bold">ระบบแจ้งปัญหา IT</h1> <p className="mt-2 text-teal-100"> แจ้งและติดตามปัญหาการใช้งานระบบภายใน </p> </div></header>
- Header ชั้นนอกกำหนดสีและระยะห่างเต็มความกว้าง
<div>ชั้นในจำกัดความกว้างและจัดเนื้อหาให้อยู่กลาง
text-3xl และ font-bold กำหนดขนาดและความหนาของหัวข้ออย่างชัดเจน
Slide 15: จัด Main ของหน้า Home
<main className="mx-auto grid grid-cols-1 max-w-5xl gap-6 px-6 py-8"> <IssueForm /> <IssueList issues={issues} /></main>
Form และ List จะเรียงลงมา โดย gap-6 เว้นระยะระหว่างสองส่วน และ max-w-5xl จำกัดความกว้างของหน้า
คงกฎ main ใน globals.css ไว้ให้ Route อื่น ส่วนหน้า Home ใช้ Utility ทับค่าพื้นฐานแล้ว
Slide 16: จัด Footer ของหน้า Home
<footer className="px-6 py-8 text-center text-sm text-slate-500"> <p>ฝ่ายเทคโนโลยีสารสนเทศ</p></footer>
text-center จัดข้อความกลางหน้า และ text-sm text-slate-500 กำหนดขนาดและสีของ Footer
Slide 17: แปลงโครง IssueForm
แสดงเฉพาะ Tag เปิดที่ต้องแก้และระดับการซ้อน: คงเนื้อหาและ Tag ปิดเดิมไว้ โดย h2 ปิดก่อน form และ legend ปิดก่อน div
<section className="rounded-lg border border-slate-200 bg-white p-6"> <h2 className="text-xl font-bold text-slate-950"> <form className="mt-4"> <fieldset className="grid gap-4"> <legend className="text-sm font-semibold text-slate-700"> <div className="grid gap-4 md:grid-cols-2">
<div> บรรทัดสุดท้ายคือ .form-row เดิมที่ครอบชื่อและอีเมล ไม่ใช่ .form-group ของแต่ละช่อง
md:grid-cols-2 เริ่มสองคอลัมน์ที่ 768px ส่วน CSS เดิมเริ่มที่ 720px
Slide 18: ปรับช่องกรอก
<div className="grid gap-2"> <label htmlFor="title" className="text-sm font-semibold text-slate-800"> หัวข้อปัญหา </label> <input id="title" name="title" type="text" required className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-teal-700 focus:outline-none focus:ring-4 focus:ring-teal-100" /></div>
แทน className="form-group" เดิมด้วย className="grid gap-2" แล้วนำ Class ชุดเดียวกันไปปรับ Field ชื่อ อีเมล และรายละเอียด โดยคง id, name, type, rows={5} และ required เดิมไว้
Textarea เพิ่ม resize-y เพื่อให้ปรับขนาดได้เฉพาะแนวตั้ง
Slide 19: ปรับปุ่มส่งฟอร์ม
<button type="submit" className="rounded-md bg-teal-700 px-4 py-3 font-bold text-white hover:bg-teal-800 focus:outline-none focus:ring-4 focus:ring-teal-100"> ส่งข้อมูล</button>
bg-teal-700คือสีปุ่มปกติhover:bg-teal-800ทำให้สีเข้มขึ้นเมื่อชี้เมาส์focus:ring-4ช่วยบอกตำแหน่งเมื่อใช้แป้นพิมพ์- ต้องเก็บ
type="submit"ไว้ เพราะปุ่มนี้ใช้ส่ง Form
Slide 20: ลบ Style ของโครงหน้าที่แทนแล้ว
ตัวอย่างย่อระบุชุดกฎ CSS ที่ต้องลบ ตั้งแต่ชื่อที่เลือกส่วนของหน้า เช่น header จนถึง } ที่ปิดชุดนั้น ไม่คัดลอก Comment ไปแทน CSS จริง
.app-nav { /* Property เดิม */ }.app-nav-inner { /* Property เดิม */ }header { /* Property เดิม */ h1 { /* Property เดิม */ }}footer { /* Property เดิม */ }.issue-list { /* margin-top: 24px; */ }
ลบ header ทั้ง Block รวม h1 ที่ซ้อนอยู่ด้านใน ส่วน .issue-list ต้องลบตอนนี้เพราะ gap-6 ของ <main> ทำหน้าที่เว้นระยะระหว่าง Form กับ List แล้ว หากเก็บไว้จะเกิดระยะซ้ำ
เก็บ main ไว้ให้ /issues, /issues/new, /issues/[id] และหน้า Not Found ที่ยังไม่ได้ใส่ Utility ของ Layout
เก็บ section ไว้ให้ IssueList จนถึง Hour 2 และเก็บวงเล็บปิดของ @layer base ไว้
Slide 21: ลบ CSS เดิมของช่องกรอก
ตัวอย่างย่อระบุ Block ที่ต้องลบทั้งก้อน ไม่คัดลอก Comment ไปแทน CSS จริง
fieldset { /* Property เดิม */ }legend { /* Property เดิม */ }.form-group { /* Property เดิม */ }label { /* Property เดิม */ }input, textarea { /* Property เดิม */ }input:focus, textarea:focus { /* Property เดิม */ }textarea { /* resize: vertical; */ }
หลังลบ ช่องกรอกควรยังมีเส้นขอบ ระยะภายใน และเส้นรอบเมื่อเลือกช่อง จาก Utility ที่เพิ่มไว้
Slide 22: ลบ Style เดิมของปุ่มและ Form Row
ตัวอย่างย่อแสดงกฎ CSS ที่เขียนซ้อนกัน ให้ลบทั้งสองชุด ไม่คัดลอก Comment ไปแทน CSS จริง
button { /* Property เดิม */ &:hover { /* Property เดิม */ }}@media (min-width: 720px) { .form-row { /* Property เดิม */ }}
เงื่อนไขขนาดจอ @media นี้มีเพียง .form-row จึงลบทั้งชุดได้ หลังลบปุ่มควรยังเปลี่ยนสีเมื่อชี้เมาส์และมีเส้นรอบเมื่อถูกเลือก
หลังลบ Form ควรยังมี Style จาก Utility ส่วน Import, @layer base, *, body, main, section, Table และ Status ยังอยู่
Slide 23: ตรวจผลลัพธ์หลังจบ Hour 1
- Navigation ยังแสดงทุกหน้า
- หน้า Home, Main และ Footer มีระยะห่างถูกต้อง
- ที่ความกว้าง
375pxชื่อและอีเมลเรียงลงมา ส่วนที่1024pxเรียงสองคอลัมน์ - กด Tab ผ่านช่องกรอกและปุ่มแล้วเห็น Focus ชัดเจน
- หน้าอื่นยังมีระยะขอบจาก
mainเดิม และตารางยังแสดงข้อมูลครบ - แท็บ Console ในเครื่องมือนักพัฒนาของ Browser และ Terminal ที่รันคำสั่ง ต้องไม่มีข้อความผิดพลาด
Form ยังไม่บันทึกข้อมูล เพราะชั่วโมงนี้ปรับเฉพาะ Style
Hour ถัดไปเราจะย้าย Style ของ StatusBadge และ IssueList แล้วทำ Table ให้รองรับหน้าจอเล็ก