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: Recap จาก Day 2

หลังจบ Day 2 Project ของเรามี:

  • Component IssueForm, IssueList และ StatusBadge
  • Type และ Mock Data ที่แยกเป็นไฟล์กลาง
  • หน้า /, /issues, /issues/new และ /issues/[id]
  • Navigation ที่ใช้ร่วมกันทุกหน้าผ่าน app/layout.tsx
  • Custom CSS ทั้งหมดอยู่ใน app/globals.css

Project ทำงานได้แล้ว แต่เมื่อ Component เพิ่มขึ้น CSS กลางจะยาวขึ้นและต้องสลับไฟล์ไปมาระหว่าง TSX กับ CSS

Hour นี้เราจะเริ่มย้าย Style มาไว้ใกล้ UI ด้วย 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 ให้เราใช้ Class สำเร็จรูปที่แทน CSS Property โดยตรง:

<button className="bg-teal-700 px-4 py-3 text-white">
  ส่งข้อมูล
</button>

ผลลัพธ์ใกล้เคียงเดิม แต่เห็น Style อยู่ข้าง Element ที่ใช้งานจริง


Slide 3: Tailwind CSS คืออะไร

ตัวอย่างเพื่อทำความเข้าใจ: ยังไม่ต้องแก้ Project ใน Slide นี้

Tailwind CSS คือ CSS Framework แบบ Utility-first คำว่า Utility หมายถึง Class ขนาดเล็กที่ทำหน้าที่อย่างใดอย่างหนึ่ง:

  • p-6 ใช้กำหนด padding
  • bg-white ใช้กำหนดสีพื้นหลัง
  • text-sm ใช้กำหนดขนาดตัวอักษร
  • font-bold ใช้กำหนดน้ำหนักตัวอักษร
  • rounded-lg ใช้กำหนดความโค้งของมุม
  • grid ใช้กำหนด display: grid

เรานำ Utility หลายตัวมาต่อกันเพื่อสร้าง Style ของ Element:

<section className="rounded-lg border border-slate-200 bg-white p-6">
  ...
</section>

Tailwind ไม่ได้แทนที่ความรู้ CSS แต่เป็นอีกวิธีหนึ่งในการนำแนวคิด CSS มาใช้


Slide 4: ตรวจว่า Tailwind พร้อมใช้งาน

ลงมือทำ: เปิดไฟล์เพื่อตรวจเท่านั้น หากค่าตรงกับตัวอย่างไม่ต้องแก้ไข

Project นี้สร้างจาก create-next-app พร้อม Recommended Defaults จึงมี Tailwind มาให้แล้ว ไม่ต้องติดตั้งซ้ำ

ตรวจใน package.json ว่ามี:

"tailwindcss": "...",
"@tailwindcss/postcss": "..."

ตรวจทั้งไฟล์ postcss.config.mjs:

const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};
 
export default config;

@tailwindcss/postcss ทำหน้าที่ประมวลผล Tailwind Class ให้กลายเป็น CSS ที่ Browser ใช้งานได้


Slide 5: เปิดใช้งาน Tailwind อีกครั้ง

ลงมือแก้ Project: app/globals.css

Day 2 เราลบ Tailwind Import ออกชั่วคราวเพื่อใช้ Custom CSS เพียงชุดเดียว ตอนนี้ให้เพิ่มบรรทัดนี้ไว้ บนสุด ของ app/globals.css:

@import "tailwindcss";

ใน app/layout.tsx ต้องยังมี Import ของ Global CSS:

import "./globals.css";

Flow การทำงานคือ:

layout.tsx โหลด globals.css

globals.css โหลด Tailwind

Tailwind Class ใน TSX เริ่มทำงาน

หลังเปิด Tailwind หน้าตาบางส่วนอาจเปลี่ยนเล็กน้อยจาก Preflight ซึ่งเป็น Style พื้นฐานที่ช่วยให้ Browser แต่ละตัวเริ่มจากค่าที่ใกล้เคียงกัน


Slide 6: ใช้ Tailwind ตอนไหนและไฟล์ไหน

หลังเพิ่ม @import "tailwindcss"; แล้ว เราสามารถเริ่มใช้ Tailwind ได้ทันที โดยเขียน Utility Class ใน className ของ Element ภายในไฟล์ .tsx:

<h1 className="text-3xl font-bold text-slate-950">
  ระบบแจ้งปัญหา IT
</h1>

Hour นี้เราจะใช้ Tailwind ตามลำดับ:

ไฟล์ส่วนที่จะปรับ
app/layout.tsxNavigation ที่ทุกหน้าใช้ร่วมกัน
app/page.tsxHeader, Main และ Footer ของหน้า Home
components/IssueForm.tsxโครง Form, Field และ Button

ไม่ต้อง Import Tailwind ในแต่ละไฟล์ TSX เพราะ app/layout.tsx โหลด globals.css ให้ทุกหน้าแล้ว ส่วน IssueList และ StatusBadge จะเริ่มใช้ Tailwind ใน Hour 2

Code <h1> ด้านบนเป็นเพียงตัวอย่าง เรายังไม่ต้องนำไปวางในไฟล์จริง โดยจะเริ่มแก้ UI ของ Project ตั้งแต่ Slide 13


Slide 7: globals.css ยังจำเป็น

แม้เริ่มใช้ Tailwind แล้ว globals.css ยังมีหน้าที่:

  • Import Tailwind ด้วย @import "tailwindcss";
  • เก็บ Style ที่มีผลกับทั้งระบบ เช่น body
  • เก็บ Custom CSS ของส่วนที่ยังไม่ได้ย้าย

หลักการย้าย Style ในชั่วโมงนี้:

  1. เพิ่ม Tailwind Class ให้ Element หนึ่งส่วน
  2. ตรวจหน้าเว็บว่าผลลัพธ์ถูกต้อง
  3. ลบ CSS เดิมของส่วนนั้น
  4. ทำซ้ำกับส่วนถัดไป

อย่าลบ CSS เดิมทั้งไฟล์ในครั้งเดียว เพราะ Component ที่ยังไม่ได้แปลงอาจเสีย Style


Slide 8: 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 หมายถึง Padding ตามแกน X และ Y
  • m หมายถึง Margin
  • gap หมายถึงช่องว่างระหว่างลูกของ Flex หรือ Grid

ตัวเลขมากขึ้นหมายถึงระยะห่างมากขึ้น เช่น p-6 กว้างกว่า p-4


Slide 9: 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 ทำให้มุมกล่องโค้ง

สีที่ลงท้ายด้วยตัวเลขน้อยจะอ่อนกว่า ส่วนตัวเลขมากจะเข้มกว่า เช่น slate-200 อ่อนกว่า slate-950


Slide 10: Utility สำหรับ Layout

ตัวอย่างสำหรับฝึกอ่าน 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 เหมาะกับการจัดวาง Element ในแนวเดียว เช่น Navigation หรือ Header ที่มีข้อมูลฝั่งซ้ายและขวา ส่วน Grid จะใช้ใน Slide ถัดไปเพื่อจัด Layout หลายคอลัมน์


Slide 11: Responsive Prefix

ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องแก้ Project

Tailwind ใช้แนวคิด Mobile-first:

  • Class ที่ไม่มี Prefix ทำงานกับทุกขนาดจอ
  • sm: เริ่มทำงานตั้งแต่จอเล็กขึ้นไป
  • md: เริ่มทำงานตั้งแต่จอกลางขึ้นไป
  • lg: เริ่มทำงานตั้งแต่จอใหญ่ขึ้นไป
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
  ...
</div>

อ่านตามลำดับ:

grid-cols-1    → ค่าเริ่มต้นเรียง 1 คอลัมน์
md:grid-cols-2 → เมื่อจอกว้างถึง md เปลี่ยนเป็น 2 คอลัมน์

จอเล็กจึงอ่านง่ายก่อน แล้วค่อยเพิ่มจำนวนคอลัมน์เมื่อมีพื้นที่


Slide 12: State Prefix

ตัวอย่างสำหรับฝึกอ่าน Class: ยังไม่ต้องแก้ Project

State Prefix ทำให้ Style ทำงานเฉพาะตอน Element อยู่ในสถานะหนึ่ง:

<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 มาที่ Element
  • disabled: ทำงานเมื่อ Element ถูกปิดการใช้งาน

State ไม่ได้มีไว้เพื่อตกแต่งอย่างเดียว แต่ช่วยบอกผู้ใช้ว่าตอนนี้กำลังโต้ตอบกับ Element ใด


Slide 13: แปลง Navigation เป็น Tailwind

แก้บางส่วน: app/layout.tsx

แทน <nav>...</nav> เดิมด้วย:

<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 className="rounded-md px-3 py-2 font-semibold text-slate-700 hover:bg-slate-100" href="/">
      หน้าแรก
    </Link>
    <Link className="rounded-md px-3 py-2 font-semibold text-slate-700 hover:bg-slate-100" href="/issues">
      รายการปัญหา
    </Link>
    <Link className="rounded-md px-3 py-2 font-semibold text-slate-700 hover:bg-slate-100" href="/issues/new">
      แจ้งปัญหาใหม่
    </Link>
  </div>
</nav>

Navigation อยู่ใน Layout เพราะทุกหน้าใช้ร่วมกัน ส่วน flex-wrap ช่วยให้เมนูขึ้นบรรทัดใหม่เมื่อพื้นที่ไม่พอ

หลังตรวจหน้าเว็บแล้ว ให้ลบ .app-nav และ .app-nav-inner เดิมออกจาก app/globals.css


Slide 14: แปลง Header เป็น Tailwind

แก้บางส่วน: app/page.tsx

แทน <header>...</header> เดิมด้วย:

<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> ชั้นในจำกัดความกว้างและจัดเนื้อหาให้อยู่กลาง

เมื่อผลลัพธ์ถูกต้อง ให้ลบ CSS เดิมของ header, header h1 และ header p


Slide 15: จัด Main และ Footer

แก้บางส่วน: app/page.tsx

เพิ่ม Tailwind Class ให้ <main> เดิม:

<main className="mx-auto grid grid-cols-1 max-w-5xl gap-6 px-6 py-8">
  <IssueForm />
  <IssueList issues={issues} />
</main>

เพิ่ม Tailwind Class ให้ <footer> เดิม:

<footer className="px-6 py-8 text-center text-sm text-slate-500">
  <p>ฝ่ายเทคโนโลยีสารสนเทศ</p>
</footer>
  • grid grid-cols-1 gap-6 ทำให้ Form และ List เรียงลงมาโดยมีระยะห่างสม่ำเสมอ
  • grid-cols-1 ทำให้ Grid Column ย่อตามความกว้างหน้าจอได้ เพื่อไม่ให้ Table ดันทั้งหน้าให้ล้นแนวนอน
  • max-w-5xl ช่วยไม่ให้เนื้อหากว้างเกินไปบนหน้าจอขนาดใหญ่

ตรวจหน้าเว็บแล้วจึงลบ CSS เดิมของ main, footer และ section + section


Slide 16: แปลงโครง IssueForm

แก้บางส่วน: components/IssueForm.tsx

เพิ่ม Tailwind Class ให้ Element เดิม โดยไม่เปลี่ยนชื่อ Field:

<section className="rounded-lg border border-slate-200 bg-white p-6">
  <h2 className="text-xl font-bold text-slate-950">แจ้งปัญหาใหม่</h2>
 
  <form className="mt-4">
    <fieldset className="grid gap-4">
      <legend className="text-sm font-semibold text-slate-700">
        ข้อมูลปัญหา
      </legend>
 
      <div className="grid gap-4 md:grid-cols-2">
        {/* ชื่อและอีเมลผู้แจ้ง */}
      </div>
 
      {/* หัวข้อ รายละเอียด และปุ่ม */}
    </fieldset>
  </form>
</section>

md:grid-cols-2 ทำให้ชื่อและอีเมลเรียงแนวตั้งบนมือถือ แล้วเปลี่ยนเป็นสองคอลัมน์เมื่อจอกว้างขึ้น


Slide 17: แปลง Field

ลงมือแก้ Project: components/IssueForm.tsx

ใช้รูปแบบนี้กับ Input และ Textarea ทุก Field:

<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>

ใช้ grid gap-2 ครอบ Label กับช่องกรอก แล้วนำ Class ชุดเดียวกันไปปรับ Field ชื่อ อีเมล และรายละเอียด โดยคง id, name, type และ required เดิมไว้

สำหรับ <textarea> ให้ใช้ Class ชุดเดียวกับ Input และเพิ่ม resize-y เพื่อให้ผู้ใช้ปรับขนาดได้เฉพาะแนวตั้งแทน CSS เดิม resize: vertical


Slide 18: แปลง Button

แทนปุ่มเดิมใน components/IssueForm.tsx ด้วย:

<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 ช่วยบอกตำแหน่งเมื่อใช้ Keyboard
  • ต้องเก็บ type="submit" ไว้ เพราะปุ่มนี้ใช้ส่ง Form

Slide 19: ลบ CSS เดิมของ Page Shell

หลังตรวจ Navigation, Header, Main และ Footer แล้ว ให้ลบ CSS Block เหล่านี้ทั้งก้อนออกจาก app/globals.css:

.app-nav { ... }
.app-nav-inner { ... }
 
header { ... }
header h1 { ... }
header p { ... }
 
main { ... }
footer { ... }
footer p { ... }
 
section + section { ... }

ยังไม่ลบ section เพราะ IssueList ยังใช้ Style ของ Selector นี้อยู่จนถึง Hour 2


Slide 20: ลบ CSS เดิมของ Form

เมื่อ IssueForm.tsx ใช้ Tailwind ครบแล้ว ให้ลบ CSS Block เหล่านี้:

fieldset { ... }
legend { ... }
.form-group { ... }
label { ... }
 
input,
textarea { ... }
 
textarea { ... }
 
input:focus,
textarea:focus { ... }
 
button { ... }
button:hover { ... }

ลบ Media Query ของ .form-row ด้วย เพราะแทนที่ด้วย md:grid-cols-2 แล้ว:

@media (min-width: 720px) {
  .form-row { ... }
}

ถ้า @media นี้มีเพียง .form-row ให้ลบทั้ง Block แต่ถ้ามีกฎอื่นอยู่ด้วย ให้ลบเฉพาะ .form-row

ยังเก็บ @import "tailwindcss";, *, body, section, .table-wrapper, Table และ .status-* ไว้ก่อน ส่วนของ Issue List จะย้ายใน Hour 2


Slide 21: ตรวจผลลัพธ์หลังจบ Hour 1

ตรวจหน้า /, /issues, /issues/new และ /issues/001:

  • Navigation ยังแสดงทุกหน้า
  • หน้า Home, Main และ Footer มีระยะห่างถูกต้อง
  • Form อ่านง่ายทั้งจอเล็กและจอกว้าง
  • Browser ไม่มี Scroll แนวนอนจาก Layout
  • Console และ Terminal ไม่มี Error

Hour ถัดไปเราจะย้าย Style ของ StatusBadge และ IssueList แล้วทำ Table ให้รองรับหน้าจอเล็ก