FileEditViewTerminal
IT Issue Bootcamp - Visual Studio Code
day-3-hour-1.mdx

Day 3 / Hour 1

Tailwind Setup and Utilities

Tailwind setup, spacing, typography, colors, layout, and state utilities.

60 minutes
Learners start converting the UI to Tailwind.

Tailwind Setup and Utilities

Day 3 - ชั่วโมงที่ 1: เปลี่ยนจาก Custom CSS มาใช้ Tailwind

เป้าหมายของชั่วโมงนี้

หลังจบชั่วโมงนี้ ผู้เรียนควรสามารถ:

  1. อธิบายได้ว่า Tailwind CSS ช่วยเขียน Style อย่างไร
  2. ตรวจและเปิดใช้งาน Tailwind ใน Next.js Project ได้
  3. อ่าน Utility Class พื้นฐานได้โดยเชื่อมโยงกับ CSS ที่เคยเรียน
  4. แปลงโครงหน้าและ Form จาก Custom CSS เป็น Tailwind ทีละส่วนได้
  5. ลบ CSS เดิมเฉพาะส่วนที่ย้ายเสร็จแล้วได้อย่างปลอดภัย

ไฟล์ที่ใช้

package.json
postcss.config.mjs
app/globals.css
app/layout.tsx
app/page.tsx
components/IssueForm.tsx

Slide 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 ของส่วนที่ยังไม่ได้ย้าย

  1. เพิ่ม Utility ใน className ของ Tag เดิม
  2. เปิดหน้าเว็บเพื่อดูผล โดย CSS เดิมอยู่ใน @layer base
  3. เมื่อย้ายครบส่วนแล้ว ค่อยลบกฎที่ไม่ใช้ในช่วงท้ายชั่วโมง

เก็บ 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 ให้รองรับหน้าจอเล็ก