บทเรียน 59 · Frontend · Basic HTML & CSS

CSS Selector — tag, class, id เลือกอันไหนเมื่อไหร่

~18 นาที · ควรผ่าน บทเรียน 58 (ฟอร์ม) มาก่อน เพราะบทนี้จะเรียกชื่อ element อย่าง h1 p div โดยไม่อธิบายซ้ำ

บทนี้มาจากสไลด์ HTML/CSS หน้า 27–36 ซึ่งเปิดหัวข้อ Basic CSS แล้วแนะนำ selector สามแบบ: tag · class · id
สไลด์บอกว่าแต่ละแบบเลือกอะไร แต่ไม่ได้บอกสิ่งที่ทำให้มือใหม่หัวหมุนที่สุด — ถ้าสองกฎสั่งสีคนละสีใส่ element เดียวกัน ใครชนะ
บทนี้จะพาไปให้ถึงตรงนั้น: อ่านกฎ CSS หนึ่งข้อให้ออก → รู้จัก selector สามแบบ → เข้าใจว่าทำไม id ชนะ class ชนะ tag → แล้วจบด้วยคำแนะนำที่ขัดใจคนเพิ่งเรียน คือ "อย่าใช้ id จัดสไตล์"
ทุกกล่อง ผลลัพธ์จริง ในหน้านี้คือโค้ดชุดเดียวกับที่โชว์ ถูกเรนเดอร์สด ๆ ในเบราว์เซอร์ของคุณเอง

🧰 ของที่ใช้ตรวจในบทนี้

Google Chrome 151.0.7922.174  # ใช้ตรวจสีที่ออกมาจริงทุกตัวอย่างในบทนี้
MDN — CSS selectors / Specificity  # ใช้ตรวจว่าตัวเลขน้ำหนักถูกต้อง

สีที่บทนี้บอกว่า "ออกมาเป็นสีนี้" ผมไม่ได้เขียนจากความจำ — ผมรันด้วย getComputedStyle() ใน Chrome จริงแล้วก็อปค่าที่ได้มาใส่ และกล่อง ผลลัพธ์จริง ข้างล่างแต่ละกล่องก็เรนเดอร์โค้ดชุดเดียวกันให้คุณเห็นซ้ำอีกที

1 · CSS คืออะไร — HTML บอกว่า "มีอะไร" CSS บอกว่า "หน้าตายังไง"

สไลด์หน้า 28 นิยาม CSS ไว้บรรทัดเดียวว่า "controls how your website looks" — คุมว่าเว็บของคุณหน้าตาเป็นยังไง
ชื่อเต็มของมันคือ Cascading Style Sheets

แปดบทที่ผ่านมาคุณเขียน HTML ซึ่งตอบคำถามว่า "หน้านี้มีอะไรบ้าง" — มีหัวข้อ มีย่อหน้า มีลิงก์ มีฟอร์ม
แต่ HTML ไม่ได้ตอบว่าหัวข้อควรตัวใหญ่แค่ไหน สีอะไร ห่างจากย่อหน้าเท่าไหร่ — ตรงนั้นเป็นงานของ CSS ล้วน ๆ

ลองดูของจริง กฎ CSS ข้างล่างนี้ยกมาจากสไลด์หน้า 28 ทั้งก้อน ส่วน HTML สองบรรทัดล่างผมถอดมาจากสกรีนช็อตหน้า 32 ที่เป็นผลของกฎชุดนี้ (เปลี่ยนแค่ชื่อคน):

<style>
  body {
    font-family: Arial, Helvetica, sans-serif;
    color: #303030;
    font-size: 16px;
  }

  h1 {
    font-weight: 600;
    font-size: 48px;
    line-height: 1;
    font-family: "Times New Roman", Times, serif;
    color: olive;
  }
</style>

<h1>Somchai Jaidee</h1>
<p>I'm a software developer</p>

สังเกตว่า HTML สองบรรทัดล่างไม่ได้บอกอะไรเรื่องสีหรือขนาดเลย มันบอกแค่ว่า "อันนี้เป็นหัวข้อใหญ่ อันนี้เป็นย่อหน้า"
ส่วนที่ทำให้หัวข้อกลายเป็นตัวหนังสือ serif สีเขียวขี้ม้าขนาด 48px คือกฎ CSS ข้างบนล้วน ๆ
ลบ <style> ทิ้ง เนื้อหายังอยู่ครบ แค่หน้าตากลับไปเป็นค่าเริ่มต้นของเบราว์เซอร์

2 · หน้าตาของกฎ CSS หนึ่งข้อ — อ่านให้ออกก่อน แล้วที่เหลือง่ายหมด

ก้อนหนึ่งก้อนข้างบนนั้นเรียกว่า กฎหนึ่งข้อ และมันมีแค่สองส่วนเสมอ ไม่มีมากกว่านี้:

h1 { color: olive; font-size: 48px; } ─┬ ─────┬───── ──────┬────── │ │ │ │ │ └─ อีกคำสั่งหนึ่ง (คั่นด้วย ; ) │ └─ คำสั่งหนึ่งคำสั่ง = ชื่อสิ่งที่จะเปลี่ยน : ค่าที่ต้องการ ; └─ "จะจัดใคร" — ตัวนี้แหละที่บททั้งบทนี้พูดถึง

ฝั่งซ้ายของปีกกาคือ "จะจัดใคร" · ฝั่งในปีกกาคือ "จัดยังไง" เขียนได้หลายคำสั่ง แต่ละคำสั่งลงท้ายด้วย ;
ทั้งบทนี้เราจะสนใจแค่ฝั่งซ้าย เพราะฝั่งขวา (สี ขนาด ระยะห่าง) ค่อยไล่จำทีหลังก็ได้ ไม่มีอะไรให้เข้าใจผิด แต่ฝั่งซ้ายมีเยอะมาก

📣 เทียบให้เห็นภาพ — ครูขานชื่อตอนเข้าแถวหน้าเสาธง

นึกภาพครูยืนหน้าเสาธงแล้วประกาศว่า "…… ยกมือขึ้น" ช่องว่างตรงนั้นคือ ฝั่งซ้ายของปีกกา และครูขานได้หลายแบบ:
· "นักเรียนชายทุกคน" — เรียกตามชนิด ใครเข้าข่ายก็โดนหมด
· "คนที่ติดปลอกแขนสีเหลือง" — เรียกตามป้ายที่เราไปติดให้เอง จะติดกี่คนก็ได้
· "เด็กชายสมชาย เลขที่ 12 ห้อง ม.4/1" — เจาะจงคนเดียวในโรงเรียน

สามแบบนี้คือ selector สามแบบที่สไลด์หน้า 30 วางไว้ข้างกันเป็นสามคอลัมน์: tag · class · id
(สไลด์เรียงคอลัมน์ว่า Tag · ID · Class ส่วนบทนี้ขอเรียง class ก่อน id เพราะไล่จาก "กว้างสุด" ไป "แคบสุด" แล้วจำง่ายกว่า — เนื้อหาเหมือนกันทุกอย่าง)
ส่วน "ยกมือขึ้น" คือฝั่งในปีกกา — คำสั่งเดียวกันเป๊ะ เปลี่ยนแค่ว่าขานเรียกใคร คนที่ทำตามก็เปลี่ยนไปแล้ว

ที่ไม่เหมือนกัน: นักเรียนได้ยินคำสั่งครั้งเดียวแล้วทำครั้งเดียวจบ แต่ CSS อ่านกฎทุกข้อในไฟล์ก่อนเสมอ แล้วค่อยตัดสินว่าจะแสดงผลยังไง
แปลว่าถ้าครูขานสองครั้งขัดกัน ("ผู้ชายยกมือ" แล้ว "สมชายเอามือลง") เด็กจริง ๆ จะงงและอาจทำตามคำสั่งหลังสุด แต่ CSS มีกติกาตายตัวว่าใครชนะ ซึ่งไม่ใช่ "คำสั่งหลังสุดชนะ" เสมอไป — เรื่องนี้อยู่ในหัวข้อ 4 และเป็นหัวใจของบทนี้

🗣 ศัพท์ที่เพิ่งเรียนไป

สี่คำนี้คือชื่อของสิ่งที่คุณเพิ่งเห็นในกล่องแผนภาพข้างบน — ใช้คำให้ตรง เวลาถามจะได้คำตอบที่ตรง

rule (กฎ)
หนึ่งก้อนเต็ม ๆ ตั้งแต่ selector ยันปีกกาปิด · หนึ่งไฟล์ CSS คือกองของ rule เรียงกันลงมา · ประโยคที่ใช้ได้เลย: "กฎตัวนี้มันไม่ติดครับ ช่วยดูให้หน่อยได้ไหม" — สั้นและตรงกว่าพูดว่า "โค้ด CSS ตรงนี้"
selector
ฝั่งซ้ายของปีกกา = "จะจัดใคร" · เป็นพระเอกของทั้งบทนี้ · ประโยคที่ใช้ได้เลย: "selector ผมเขียนถูกไหมครับ ทำไมมันไม่จับ element ตัวนั้น" · พิมพ์ถาม AI ได้ว่า "เขียน CSS selector ให้จับเฉพาะ … ให้หน่อย"
declaration (คำสั่ง) และ declaration block
คู่ property: value; หนึ่งคู่ = หนึ่ง declaration · ทุกคู่ที่อยู่ในปีกกาเดียวกันรวมเรียกว่า declaration block · ประโยคที่ใช้ได้เลย: "ใน block นี้มี declaration ตัวไหนที่ไม่จำเป็นบ้างครับ"
property / value
color คือ property (ชื่อสิ่งที่จะเปลี่ยน) · olive คือ value (ค่าที่ต้องการ) · เวลาหาข้อมูลให้ค้นด้วยชื่อ property เสมอ เช่นค้น "MDN css line-height" จะเจอหน้าเดียวจบ แทนที่จะไล่อ่านบล็อกสิบอัน

3 · selector สามแบบที่ต้องรู้ก่อนอย่างอื่น

สไลด์หน้า 30 วางสามแบบนี้ไว้ข้างกันพอดี แล้วหน้า 31–36 ค่อยขยายทีละแบบ ผมสรุปมาให้ในตารางเดียว:

แบบเขียนยังไงจับอะไรในหน้าหนึ่งมีได้กี่ตัว
tag h1 { }
(ชื่อ element เปล่า ๆ ไม่มีสัญลักษณ์นำหน้า)
ทุก element ที่เป็นชนิดนั้น ทั้งหน้า กี่ตัวก็ได้ — ไม่ได้เลือก มันจับหมด
class .sub-title { }
(นำหน้าด้วยจุด)
ทุก element ที่เราไปเขียน class="sub-title" ให้ กี่ตัวก็ได้ และหนึ่ง element ติดได้หลาย class
id #about-me { }
(นำหน้าด้วยชาร์ป)
element ตัวเดียวที่เขียน id="about-me" ไว้ ชื่อหนึ่ง ๆ ห้ามซ้ำในหน้าเดียวกัน

ตัวอย่างข้างล่างนี้ผมประกอบขึ้นจากสไลด์หน้า 33–36 — หน้าโปรไฟล์ที่มีสอง section โดยเอา .sub-title จากหน้า 33 มารวมกับ #about-me จากหน้า 35 แล้วเติมกฎ tag ของ h4 เข้าไปเองเพื่อให้เห็นครบทั้งสามแบบในตัวอย่างเดียว (สไลด์ใช้ h3 เป็นหัวข้อย่อย ผมเปลี่ยนเป็น h4 เฉย ๆ เพราะหน้าบทเรียนที่คุณอ่านอยู่นี้ใช้ h3 เป็นหัวข้อจริงของตัวเองอยู่แล้ว — เปลี่ยนแค่ชื่อ tag ไม่ได้เปลี่ยนหลักการอะไรเลย):

<style>
  /* tag — จับ h4 ทุกตัวในหน้า */
  h4 { font-family: Georgia, serif; }

  /* class — จับทุกตัวที่ติดป้าย sub-title (ที่นี่มีสองตัว) */
  .sub-title { font-size: 24px; }

  /* id — จับตัวเดียว คือกล่องที่ id ชื่อ about-me */
  #about-me { background-color: #efefbc; padding: 16px; }
</style>

<div id="about-me">
  <h4 class="sub-title">About Me</h4>
  <p>Product design and web development.</p>
</div>
<div id="experience">
  <h4 class="sub-title">Experience</h4>
  <p>Dec 2023 - Present</p>
</div>

อ่านผลที่ออกมาแล้วจะเห็นความต่างของสามแบบชัดมาก:

⚠️ จุดที่มือใหม่พลาดบ่อยที่สุดในบทนี้ — เครื่องหมายนำหน้า

ใน HTML เราเขียน class="sub-title" และ id="about-me" ไม่มีจุด ไม่มีชาร์ป
แต่ใน CSS ต้องเขียน .sub-title และ #about-me มีจุด มีชาร์ป
ลืมจุดเมื่อไหร่ sub-title { } จะกลายเป็นการเรียกหาชนิด element ชื่อ sub-title ซึ่งไม่มีอยู่จริง → กฎนั้นไม่ทำงาน และเบราว์เซอร์ไม่แจ้ง error ให้ด้วย มันเงียบไปเฉย ๆ นี่คือสาเหตุอันดับหนึ่งของอาการ "เขียน CSS แล้วไม่เห็นอะไรเปลี่ยน"

🗣 ศัพท์ที่เพิ่งเรียนไป
type selector (หรือที่สไลด์เรียกว่า tag selector)
selector ที่เป็นชื่อ element เปล่า ๆ เช่น p h1 div · ในสเปกทางการเรียก type selector แต่คนทำงานเรียก tag กันหมด — พูดได้ทั้งคู่ ไม่มีใครงง · ประโยคที่ใช้ได้เลย: "อันนี้ตั้ง tag selector ไว้ที่ระดับ global เลยเหรอครับ"
class
ป้ายที่เราตั้งชื่อเองแล้วแปะให้ element กี่ตัวก็ได้ — สไลด์บอกว่าคือ "a reusable label" ซึ่งคำสำคัญคือ reusable · หนึ่ง element ติดได้หลายป้ายพร้อมกันด้วย class="btn danger" (คั่นด้วยเว้นวรรค) · ประโยคที่ใช้ได้เลย: "ควรทำเป็น class ที่ใช้ซ้ำได้ไหมครับ หรือเขียนเฉพาะที่นี่พอ"
id
ชื่อเฉพาะตัวของ element หนึ่งตัว ห้ามซ้ำในหน้าเดียวกัน · งานหลักจริง ๆ ของมันไม่ใช่การจัดสไตล์ (หัวข้อ 6 จะบอกว่าทำไม) แต่คือการชี้ตำแหน่ง — ลิงก์ href="#about-me" กระโดดมาที่นี่ · <label for="email"> ผูกกับช่องกรอก · JavaScript หาด้วย getElementById · ประโยคที่ใช้ได้เลย: "id ตัวนี้มีอะไรอ้างถึงอยู่หรือเปล่าครับ ก่อนผมจะเปลี่ยนชื่อ"

4 · สองกฎสั่งคนละสีใส่ตัวเดียวกัน — ใครชนะ

ตรงนี้คือจุดที่สไลด์ข้ามไป แต่เป็นจุดที่คุณจะเจอภายในวันแรกที่เขียน CSS จริง
สมมติหัวข้อย่อยตัวหนึ่ง (<h4>) ติดทั้ง class และ id แล้วมีกฎสามข้อสั่งสีคนละสี — สีไหนออกมา?

<style>
  h4 { color: teal; }
  .warning { color: orange; }
  #top { color: crimson; }
</style>

<h4>ผมมีแค่ tag</h4>
<h4 class="warning">ผมมี class ด้วย</h4>
<h4 class="warning" id="top">ผมมีครบทั้งสาม</h4>

ผมวัดสีที่ออกมาจริงด้วย getComputedStyle() ใน Chrome ได้ตามนี้:

บรรทัด 1 (tag เปล่า)        rgb(0, 128, 128)   # teal
บรรทัด 2 (tag + class)      rgb(255, 165, 0)   # orange — class ทับ tag
บรรทัด 3 (tag + class + id) rgb(220, 20, 60)   # crimson — id ทับทั้งคู่

บรรทัดที่ 3 คือคำตอบของทั้งบท: id ชนะ class · class ชนะ tag
และที่สำคัญกว่านั้น — มันไม่เกี่ยวกับว่าใครเขียนก่อนเขียนหลังเลย ลองสลับสามบรรทัดนั้นให้ #top ขึ้นก่อนสุด ผลก็ยัง crimson เหมือนเดิม (ผมรันมาแล้ว: id เขียนบรรทัดแรก class เขียนบรรทัดสุดท้าย ยังได้ rgb(220, 20, 60))

📋 เทียบให้เห็นภาพ — ประกาศสามระดับในออฟฟิศ

นึกภาพว่าคุณเข้าออฟฟิศวันจันทร์แล้วเจอประกาศสามใบพร้อมกัน:
· ใบที่ติดหน้าลิฟต์ — "พนักงานทุกคนใส่เสื้อโปโลได้"
· ใบที่ติดหน้าห้องแผนกคุณ — "แผนกนี้วันจันทร์ใส่เสื้อเชิ้ต"
· โน้ตที่หัวหน้าแปะบนโต๊ะคุณโดยเฉพาะ — "วันนี้คุณใส่สูท ไปเจอลูกค้า"

คุณจะใส่สูท ไม่ใช่เพราะโน้ตนั้นเขียนทีหลัง แต่เพราะมันเจาะจงตัวคุณมากที่สุด — ประกาศหน้าลิฟต์พูดถึงคนเป็นร้อย ประกาศหน้าแผนกพูดถึงคนสิบคน โน้ตบนโต๊ะพูดถึงคุณคนเดียว
CSS ตัดสินด้วยหลักเดียวกันเป๊ะ: selector ที่เจาะจงกว่า ชนะ
tag = ประกาศหน้าลิฟต์ · class = ประกาศหน้าแผนก · id = โน้ตบนโต๊ะคุณ

ที่ไม่เหมือนกัน: ในออฟฟิศจริง ถ้าเจอประกาศสองใบเจาะจงเท่ากัน คุณต้องเดินไปถามหัวหน้าว่าเอาอันไหน
แต่ CSS ตัดสินเองได้เสมอ ไม่มีคำว่า "ก้ำกึ่ง" — ถ้าเจาะจงเท่ากันเป๊ะ อันที่เขียนทีหลังชนะ
อีกจุดที่ต่าง: โน้ตบนโต๊ะแค่ทับเรื่องเสื้อผ้า เรื่องอื่นยังใช้ประกาศหน้าลิฟต์อยู่ — CSS ก็เหมือนกัน มันทับกันทีละ property ไม่ใช่ทับทั้งกฎ ถ้า #top สั่งแค่ color ส่วน h4 สั่ง font-size ด้วย ตัวหนังสือก็ยังได้ font-size จาก h4 อยู่ดี

ตัวเลขน้ำหนักที่ dev ใช้คุยกันจริง

เวลาคุยงานจริง คนจะเขียนน้ำหนักเป็นตัวเลขสามช่อง (id, class, tag) แบบนี้:

selectorน้ำหนัก (id, class, tag)อ่านว่า
h40, 0, 1tag หนึ่งตัว
.warning0, 1, 0class หนึ่งตัว
#top1, 0, 0id หนึ่งตัว
h4.warning0, 1, 1class หนึ่ง + tag หนึ่ง
.btn.danger0, 2, 0class สองตัวบน element เดียวกัน
body div p0, 0, 3tag สามตัวซ้อนกัน

วิธีเทียบคือ ดูช่องซ้ายสุดก่อน ใครมากกว่าชนะทันที ไม่ต้องดูช่องขวาเลย ถ้าเท่ากันค่อยขยับไปดูช่องถัดไป

⚠️ กับดัก: สามช่องนี้ไม่ใช่เลขฐานสิบ — tag สิบตัวก็ยังแพ้ class ตัวเดียว

คนเพิ่งเรียนชอบคิดว่า 0,0,3 คือ "สามคะแนน" แล้ว 0,1,0 คือ "สิบคะแนน" เลยเดาว่าถ้าซ้อน tag เยอะ ๆ สักสิบเอ็ดชั้นก็น่าจะแซง class ได้
แซงไม่ได้ ช่องซ้ายกว่าชนะเสมอไม่ว่าช่องขวาจะเยอะแค่ไหน ผมรันพิสูจน์แล้ว:

<style>
  .mark { color: green; }
  body div p { color: red; }
</style>

<div>
  <p>ผมโดน body div p</p>
  <p class="mark">ผมมี class ชื่อ mark</p>
</div>

บรรทัดบนออกมา rgb(255, 0, 0) = red ตามที่ body div p สั่ง — ยืนยันว่ากฎนั้นทำงานอยู่จริง ไม่ได้เขียนผิด
แต่บรรทัดล่างออกมา rgb(0, 128, 0) = green ทั้งที่ body div p เขียนทีหลัง และ มี tag ตั้งสามตัว แต่ .mark มี class หนึ่งตัวก็พอ — 0,1,0 ชนะ 0,0,3
(ผมลองยืดเป็น tag สิบเอ็ดตัวซ้อนกันแล้วด้วย ผลยังเป็น green เหมือนเดิม)

5 · ทายก่อนเฉลย — สีอะไรออกมา

ตอนนี้คุณรู้กติกาครบแล้ว ลองใช้กับโจทย์ที่มีกับดักสองชั้นซ้อนกัน

<style>
  .pill { color: red; }
  p     { color: blue; }
  .pill { color: green; }
</style>

<p class="pill">ผมสีอะไร?</p>

ทายว่าข้อความบรรทัดนั้นออกมาเป็นสีอะไร — red · blue · หรือ green
และตอบตัวเองให้ได้ด้วยว่า ทำไมอีกสองสีถึงแพ้ ด้วยเหตุผลคนละข้อกัน

ได้ green — ค่าที่วัดได้จริงคือ rgb(0, 128, 0)

กับดักอยู่ตรงที่สองสีที่แพ้ แพ้ด้วยกฎคนละข้อกัน ต้องแยกให้ออก:

  • blue แพ้เพราะน้ำหนัก · p คือ 0,0,1 ส่วน .pill คือ 0,1,0 → class ชนะ tag
    ตรงนี้ ไม่เกี่ยวกับลำดับเลย ต่อให้ย้าย p ไปไว้บรรทัดสุดท้ายก็ยังแพ้
  • red แพ้เพราะลำดับ · .pill ทั้งสองข้อน้ำหนัก 0,1,0 เท่ากันเป๊ะ พอเสมอกัน CSS ถึงจะหันไปดูว่าใครเขียนทีหลัง → บรรทัดล่างชนะ
    ตรงนี้ เกี่ยวกับลำดับเต็ม ๆ ถ้าสลับสองบรรทัดนั้นจะได้ red แทน

ลำดับการตัดสินที่ต้องจำ: ดูน้ำหนักก่อนเสมอ → เสมอกันเมื่อไหร่ ค่อยดูว่าใครเขียนทีหลัง
คนที่จำสลับกันจะไปนั่งสลับบรรทัดใน CSS ทั้งวันโดยที่หน้าเว็บไม่เปลี่ยนเลย เพราะปัญหาจริงอยู่ที่น้ำหนัก ไม่ใช่ลำดับ

โค้ดชุดเดียวกันเป๊ะ เรนเดอร์จริงให้ดูอีกที:

<style>
  .pill { color: red; }
  p     { color: blue; }
  .pill { color: green; }
</style>

<p class="pill">ผมสีอะไร?</p>
🗣 ศัพท์ที่เพิ่งเรียนไป
specificity (ความเจาะจง)
คะแนนน้ำหนักสามช่อง (id, class, tag) ที่ใช้ตัดสินว่ากฎไหนชนะ · คนไทยในวงการมักพูดทับศัพท์ว่า "สเปซิฟิซิตี้" หรือ "น้ำหนัก selector" · ประโยคที่ใช้ได้เลย: "กฎผมไม่ติดเพราะ specificity สู้ไม่ได้ใช่ไหมครับ" · พิมพ์ถาม AI ได้ว่า "ช่วยคำนวณ specificity ของ selector นี้ให้หน่อย" แล้วมันจะตอบมาเป็นสามช่องเลย
cascade (การไล่ทับกัน)
ตัว C ตัวแรกในคำว่า CSS · หมายถึงกระบวนการทั้งหมดที่เบราว์เซอร์ใช้เลือกว่ากฎไหนชนะ ซึ่ง specificity เป็นแค่ขั้นตอนหนึ่งในนั้น และ source order เป็นขั้นตอนถัดไป · ประโยคที่ใช้ได้เลย: "อันนี้มันโดน cascade ทับหรือว่า selector ผมเขียนผิดครับ" — คำถามนี้แยกสองสาเหตุที่คนสับสนกันบ่อยที่สุดออกจากกันได้เลย
source order (ลำดับที่เขียน)
ตัวตัดสินสุดท้ายเมื่อ specificity เท่ากัน — อันที่อยู่ล่างกว่าชนะ · เป็นเหตุผลที่ไฟล์ CSS ของงานจริงมักเรียงจาก "กฎกว้าง ๆ" ไว้บน แล้ว "กฎเฉพาะจุด" ไว้ล่าง · ประโยคที่ใช้ได้เลย: "ไฟล์นี้ควรถูก import ก่อนหรือหลัง theme หลักครับ"
!important
คำที่เติมท้ายค่าเพื่อข้ามกติกาน้ำหนักทั้งหมด เช่น color: red !important; · ยังไม่ต้องใช้ในตอนนี้ — มันคือปุ่มโกงที่ทำให้กฎนั้นแก้ยากขึ้นไปอีกชั้น และวิธีแก้ !important คือต้องใส่ !important ทับอีกที ซึ่งลามไปทั้งไฟล์ · ให้รู้จักไว้เพื่ออ่านโค้ดคนอื่นออก และเพื่อรู้ว่าเห็นเมื่อไหร่แปลว่ามีคนเคยจนตรอกตรงนั้น

6 · แล้วทำไมถึงบอกว่า "อย่าใช้ id จัดสไตล์"

พออ่านหัวข้อ 4 จบ หลายคนจะสรุปผิดทันทีว่า "งั้นใช้ id ตลอดเลยสิ จะได้ชนะทุกอย่าง ไม่ต้องมานั่งงง"
นี่คือกับดักที่ทำให้โปรเจกต์จริงพังช้า ๆ และเป็นเหตุผลที่ทีมส่วนใหญ่มีกติกาว่า จัดสไตล์ด้วย class เป็นหลัก · ใช้ id เพื่อ "ชี้ตำแหน่ง" เท่านั้น

เหตุผลมีสามข้อ และข้อที่ 1 คือข้อที่เจ็บที่สุด:

ข้อ 1 — ชนะแรงเกินไป จนวันหลังแก้ไม่ได้

สมมติวันนี้คุณเขียนราคาสินค้าไว้แบบนี้ แล้ววันหลังทีมขอให้ "ราคาที่ลดแล้วเป็นสีแดง":

ใช้ id จัดสไตล์

#price { color: gray; }
.sale-price { color: red; }

แปะ class="sale-price" เพิ่มเข้าไปแล้ว แต่ราคายังเทาเหมือนเดิม (ค่าที่วัดได้จริง rgb(128, 128, 128))
เพราะ 1,0,0 ชนะ 0,1,0 ต่อให้เขียนทีหลังก็ไม่ช่วย
ทางออกที่เหลือมีแต่ทางแย่: ไปแก้ #price ตรง ๆ (แล้วพังที่อื่น) · เขียน #price.sale-price (เริ่มสะสมหนี้) · หรือใส่ !important (เริ่มลามทั้งไฟล์)

ใช้ class จัดสไตล์

.price { color: gray; }
.sale-price { color: red; }

น้ำหนักเท่ากันทั้งคู่ที่ 0,1,0 → ตัวที่เขียนทีหลังชนะ ราคาเป็นสีแดงตามที่ขอ
อยากให้กลับเป็นเทาก็แค่เอา class ออก
นี่คือเหตุผลที่ CSS ของงานจริงเกือบทั้งไฟล์เป็น class — เพราะน้ำหนักเท่ากันหมด ทำให้เดาผลลัพธ์ได้จากลำดับอย่างเดียว ซึ่งเป็นสิ่งที่คนอ่านโค้ดต่อจากคุณต้องการที่สุด

ข้อ 2 — id ใช้ซ้ำไม่ได้ แต่หน้าตาต้องใช้ซ้ำเสมอ

ในหน้าหนึ่ง id ชื่อหนึ่ง ๆ มีได้ตัวเดียว
แต่สิ่งที่คุณอยากจัดสไตล์แทบทั้งหมด — ปุ่ม การ์ด ป้ายสถานะ หัวข้อ section — มีมากกว่าหนึ่งอันเสมอ หรือถ้าวันนี้มีอันเดียว พรุ่งนี้ก็จะมีอันที่สอง
พอถึงวันนั้น กฎที่เขียนด้วย id จะ copy ไปใช้ต่อไม่ได้เลย ต้องนั่ง copy ทั้งบล็อกแล้วเปลี่ยนชื่อ id ซึ่งแปลว่าถ้าจะแก้สีปุ่ม ต้องไล่แก้ทุกที่ที่ copy ไป
ส่วน class ตัวเดียวแปะได้ไม่จำกัด — ตรงกับที่สไลด์หน้า 33 เขียนไว้เป๊ะว่า class คือ "a reusable label"

ข้อ 3 — id มีงานประจำของมันอยู่แล้ว

id ไม่ได้ถูกออกแบบมาเพื่อจัดสไตล์ตั้งแต่แรก มันคือที่อยู่ของ element และมีคนใช้อยู่สามกลุ่ม:

ถ้าคุณเอา id ไปผูกกับหน้าตาด้วย วันที่อยากเปลี่ยนหน้าตาคุณจะไม่กล้าเปลี่ยนชื่อ id เพราะไม่รู้ว่ามีลิงก์หรือ JS ตัวไหนอ้างอยู่บ้าง — ของสองอย่างที่เปลี่ยนคนละจังหวะกัน ไม่ควรผูกไว้ด้วยชื่อเดียวกัน

🖊 เทียบให้เห็นภาพ — ปากกาเมจิกลบไม่ออก กับกระดาษโน้ตกาว

นึกภาพจัดกล่องเก็บของในบ้าน
· ใช้ class = แปะกระดาษโน้ตกาวว่า "ของครัว" — ลอกออกได้ ย้ายไปแปะกล่องอื่นได้ แปะซ้ำสิบกล่องก็ได้
· ใช้ id = เขียนด้วยปากกาเมจิกลบไม่ออกลงบนตัวกล่องว่า "กล่องที่ 7" — ชี้ตำแหน่งได้แม่นมาก แต่วันที่อยากเปลี่ยนใจ คุณลบไม่ออก

เขียนหมายเลขกล่องด้วยเมจิกเป็นเรื่องดี — เอาไว้อ้างอิงตอนหาของ (นั่นคืองานจริงของ id)
แต่ถ้าคุณเอาเมจิกไปเขียนว่า "กล่องนี้สีฟ้า" แล้ววันหลังอยากให้เป็นสีชมพู คุณต้องหาอะไรมาปิดทับ ซึ่งก็คือ !important ในโลก CSS นั่นเอง

ที่ไม่เหมือนกัน: ปากกาเมจิกลบไม่ออกจริง ๆ แต่ id ใน CSS ยังแก้ได้ถ้าคุณยอมเปิดไฟล์ไปแก้เอง — มันไม่ได้ ทำไม่ได้ แต่แพงขึ้นทุกครั้ง ที่โปรเจกต์โตขึ้นและมีคนอื่นมาอ้างถึง id นั้นเพิ่ม
อีกจุดที่ต่าง: กระดาษโน้ตกาวแปะเยอะแล้วรก แต่ class แปะเยอะไม่รก — class="card card-wide highlight" เป็นเรื่องปกติมากและอ่านออกด้วยซ้ำว่าตั้งใจอะไร

7 · สรุป — เจอสถานการณ์ไหน หยิบอันไหน

สิ่งที่อยากทำใช้อะไรเพราะอะไร
ตั้งฟอนต์ · สีตัวหนังสือ · ขนาดพื้นฐานของทั้งหน้า tag เช่น body { } อยากให้โดนทุกตัวจริง ๆ และน้ำหนักต่ำสุดพอดี ทับทีหลังได้ง่าย
ทำให้ h1–h3 ทั้งเว็บหน้าตาเป็นชุดเดียวกัน tag ได้ทันทีโดยไม่ต้องไปแตะ HTML ทีละตัว และของที่เพิ่มทีหลังก็เข้าชุดเอง
ปุ่ม · การ์ด · ป้ายสถานะ · อะไรก็ตามที่มีมากกว่าหนึ่งอัน class ← ค่าเริ่มต้นของงานส่วนใหญ่ ใช้ซ้ำได้ · น้ำหนักเท่ากันหมดจึงเดาผลได้จากลำดับ · ถอดออกก็ง่าย
จุดเดียวในหน้าที่หน้าตาไม่เหมือนใครเลย class ที่ตั้งชื่อเฉพาะ เช่น .hero-title ได้ความเจาะจงที่ต้องการโดยไม่ต้องรับน้ำหนักของ id มาด้วย
ทำลิงก์กระโดดไปหัวข้อ · ผูก label กับช่องกรอก · ให้ JS หยิบ element id นี่คืองานที่ id ถูกออกแบบมาเพื่อมัน และไม่มีอะไรทำแทนได้

✅ กฎง่าย ๆ ที่พกไปใช้ได้เลยตั้งแต่วันนี้
เขียน id ใน HTML ได้เต็มที่ตามต้องการ — มันมีประโยชน์มาก
แต่ในไฟล์ CSS อย่าให้มี # โผล่มาเลย ถ้าวันไหนคุณรู้สึกว่า "ต้องใช้ # แล้วล่ะ ไม่งั้นสู้ไม่ได้" นั่นคือสัญญาณว่ามีกฎข้ออื่นแรงเกินไปอยู่ก่อนแล้ว — ให้ไปลดน้ำหนักกฎนั้นแทน อย่าเพิ่มน้ำหนักฝั่งเรา

8 · ทดสอบความเข้าใจ

หน้าเว็บมีปุ่ม "ลบ" อยู่ 6 จุดกระจายทั่วหน้า และอยากให้ทั้ง 6 จุดเป็นพื้นแดงเหมือนกัน วันหลังอาจมีปุ่มลบเพิ่มอีก · ควรเขียน selector แบบไหน

ตัวเลือกที่ 2 ถูก — งานนี้เข้านิยามของ class เป๊ะ ๆ คือ "หลายตัวที่ใช้หน้าตาชุดเดียวกัน" แปะป้ายเดิมให้ปุ่มที่เพิ่มมาทีหลังก็ได้ผลทันทีโดยไม่ต้องแตะ CSS อีก

ตัวเลือกที่ 1 ผิด เพราะ button จับปุ่มทุกตัวในหน้า รวมถึงปุ่ม "บันทึก" และ "ยกเลิก" ที่ไม่ควรเป็นสีแดงด้วย — มันกว้างเกินโจทย์
ตัวเลือกที่ 3 ผิด เพราะ id ชื่อหนึ่ง ๆ ห้ามซ้ำในหน้าเดียวกัน ใส่ให้ปุ่มทั้ง 6 ตัวคือ HTML ที่ผิดกติกา · จุดที่หลอกคือ สีจะติดครบทั้ง 6 ปุ่มจริง ๆ (ผมรันดูแล้ว CSS จับทุกตัวที่ id ซ้ำ ไม่ได้จับแค่ตัวแรก) หน้าเว็บเลยดูเหมือนไม่มีอะไรผิด
แต่ของอย่างอื่นที่พึ่ง id จะพังเงียบ ๆ — document.getElementById('btn-danger') คืนมาแค่ปุ่มแรกปุ่มเดียว และลิงก์ href="#btn-danger" ก็กระโดดไปได้ที่เดียว สรุปคือแลก HTML ที่ผิดกติกามาเพื่อให้ได้สิ่งที่ class ทำได้อยู่แล้ว
ตัวเลือกที่ 4 ผิด เพราะได้ผลก็จริง แต่กลายเป็นกฎซ้ำ 6 ก้อน วันที่อยากเปลี่ยนเฉดแดงต้องไล่แก้ทั้ง 6 ที่ และลืมที่ใดที่หนึ่งแน่นอน

จุดที่ต้องจำ: ถ้าของที่จะจัดสไตล์มีมากกว่าหนึ่งอัน หรือมีโอกาสจะมีอันที่สอง คำตอบคือ class เกือบทุกครั้ง

ไฟล์ CSS มีสองบรรทัดนี้เรียงกัน: .title { color: blue; } แล้วบรรทัดถัดมา .title { color: brown; } · <h2 class="title"> จะออกมาเป็นสีอะไร และเพราะอะไร

ตัวเลือกที่ 3 ถูก — .title ทั้งสองข้อมีน้ำหนัก 0,1,0 เท่ากันเป๊ะ พอ specificity ตัดสินไม่ได้ CSS ถึงจะขยับไปดูขั้นถัดไปคือ source order แล้วให้บรรทัดล่างชนะ

ตัวเลือกที่ 1 ผิด เพราะ CSS ไม่ได้ทำงานแบบ "เจอก่อนล็อกก่อน" — มันอ่านกฎให้ครบทั้งไฟล์แล้วค่อยตัดสิน และเมื่อเสมอกันมันเลือกอันหลัง ไม่ใช่อันแรก
ตัวเลือกที่ 2 ผิด เพราะกฎที่ขัดกันเป็นเรื่องปกติมากใน CSS cascade มีไว้ตัดสินกรณีแบบนี้โดยเฉพาะ ไม่มีสถานการณ์ที่เบราว์เซอร์ยอมแพ้แล้วข้ามทั้งคู่
ตัวเลือกที่ 4 ผิด ตรงเหตุผล แม้คำตอบจะบังเอิญตรง — น้ำหนักคิดจาก selector ทีละข้อ ไม่มีการเอากฎคนละข้อมาบวกกัน ถ้าจำเหตุผลผิดแบบนี้ พอเจอโจทย์ที่น้ำหนักไม่เท่ากันจะตอบผิดทันที

จุดที่ต้องจำ: source order เป็นตัวตัดสินก็ต่อเมื่อ specificity เสมอกันเท่านั้น

CSS มีสองข้อ: .mark { color: green; } เขียนไว้บรรทัดบน และ body div p { color: red; } เขียนไว้บรรทัดล่าง · <p class="mark"> ที่อยู่ใน div จะเป็นสีอะไร

ตัวเลือกที่ 1 ถูก — .mark คือ 0,1,0 ส่วน body div p คือ 0,0,3 · เทียบจากช่องซ้ายไปขวา พอถึงช่อง class ก็จบเกมแล้ว (1 ต่อ 0) ไม่ต้องดูช่อง tag เลย · ผมรันจริงใน Chrome ได้ rgb(0, 128, 0) ตามนี้

ตัวเลือกที่ 2 ผิด เพราะสามช่องนั้นไม่ใช่เลขฐานสิบที่เอามาบวกกันได้ — ต่อให้ยืดเป็น tag สิบเอ็ดตัวซ้อนกันก็ยังแพ้ class ตัวเดียว
ตัวเลือกที่ 3 ผิด เพราะ source order จะถูกใช้ก็ต่อเมื่อน้ำหนักเสมอกัน ซึ่งกรณีนี้ไม่เสมอ · การเรียงบรรทัดใหม่ไม่ช่วยอะไรเลย
ตัวเลือกที่ 4 ผิด เพราะความยาวของ selector ไม่ใช่เกณฑ์ตัดสินอะไรทั้งสิ้น ที่ตัดสินคือชนิดของสิ่งที่อยู่ใน selector ไม่ใช่จำนวนตัวอักษร

จุดที่ต้องจำ: เทียบน้ำหนักให้ไล่จากช่องซ้ายสุด ใครมากกว่าชนะทันที ช่องขวาไม่มีสิทธิ์แซง

ทำไมทีมส่วนใหญ่ถึงมีกติกาว่า "อย่าเอา id ไปจัดสไตล์ ให้ใช้ class แทน"

ตัวเลือกที่ 4 ถูก — สองเหตุผลนี้คือหัวใจ · 1,0,0 ทับด้วย class ไม่ได้เลย เหลือทางออกแค่ !important หรือเขียน selector ที่ยาวขึ้นเรื่อย ๆ · และเพราะ id ห้ามซ้ำ กฎนั้นจึงใช้ต่อกับ element ตัวที่สองไม่ได้

ตัวเลือกที่ 1 ผิด เพราะเรื่องความเร็วไม่ใช่ประเด็น — #id เป็น selector ที่เบราว์เซอร์หาได้เร็วที่สุดด้วยซ้ำ · ปัญหาของมันคือเรื่องการดูแลโค้ดล้วน ๆ
ตัวเลือกที่ 2 ผิด เพราะ #about-me { } ใช้ได้ปกติและทำงานได้จริง อย่างที่เห็นในหัวข้อ 3 · มันไม่ใช่เรื่อง "ทำไม่ได้" แต่เป็นเรื่อง "ทำได้แต่ไม่ควร"
ตัวเลือกที่ 3 ผิด เพราะ id แปะกับ element ชนิดไหนก็ได้ทั้งนั้น ทั้ง p h2 input section ไม่มีข้อจำกัดเรื่องชนิด

จุดที่ต้องจำ: id ไม่ได้ "ผิด" แต่มัน "แข็งเกินไป" — เลือก class เพื่อให้พรุ่งนี้ยังแก้ได้ง่าย

9 · ภารกิจ

🛠️ ภารกิจ — ทำให้กฎที่แพ้ กลับมาชนะ โดยห้ามแตะ id

ภารกิจนี้ไม่ใช่ "ลองพิมพ์ตาม" แต่คือสร้างอาการเสียขึ้นมาเอง แล้วพิสูจน์ว่าซ่อมได้จริง ด้วยเครื่องมือที่จะอยู่กับคุณไปตลอด คือแท็บ Elements → Styles ใน DevTools

  1. สร้างอาการเสีย: สร้างไฟล์ selector-lab.html ที่มีข้อความราคาหนึ่งบรรทัด เขียนเป็น <p id="price" class="sale-price">1,290 บาท</p>
    แล้วใน <style> ใส่สองกฎนี้ โดยให้ #price อยู่บรรทัดบน: #price { color: gray; } และ .sale-price { color: red; }
    เปิดในเบราว์เซอร์ — ต้องเห็นเป็นสีเทา ถ้าเห็นเป็นสีแดงแปลว่ายังเขียนไม่ตรงโจทย์ ให้ย้อนกลับไปดูว่าพิมพ์ # กับ . ครบไหม
  2. พิสูจน์ว่ามันแพ้จริง ไม่ใช่กฎหาย: คลิกขวาที่ข้อความ → Inspect → ดูแผง Styles ทางขวา
    ต้องเห็น color: red ถูกขีดฆ่าคาไว้ (ตัวหนังสือมีเส้นพาดกลาง)
    นี่คือหลักฐานที่แยกสองอาการที่หน้าตาเหมือนกันออกจากกันได้: ถูกขีดฆ่า = กฎติดแล้วแต่แพ้ · ไม่โผล่มาเลย = selector เขียนไม่ตรง
    วันหลังเจอ "เขียน CSS แล้วไม่เปลี่ยน" ให้เช็คตรงนี้เป็นอย่างแรกเสมอ
  3. ซ่อมโดยห้ามใช้ทางลัด: ทำให้ราคาเป็นสีแดงให้ได้ โดยมีข้อห้ามสามข้อ — ห้ามใช้ !important · ห้ามลบหรือแก้บรรทัด .sale-price · ห้ามเพิ่ม # เข้าไปในไฟล์ CSS อีกตัว
    (ใบ้: ข้อจำกัดพวกนี้บีบให้เหลือทางเดียว และเป็นทางที่หัวข้อ 6 บอกไว้แล้ว)
  4. เขียนหลักฐานยืนยัน: CSS ไม่มี unit test ให้รัน สิ่งที่ใช้แทนได้คือ คำถามที่ตอบด้วยของที่มองเห็นได้จริง — จดคำตอบสามข้อนี้ไว้ท้ายไฟล์ในคอมเมนต์ <!-- ... -->
    (ก) หลังซ่อมแล้ว ในแผง Styles บรรทัดไหนถูกขีดฆ่าแทน
    (ข) น้ำหนักของ selector ที่ชนะเป็นเท่าไหร่ในรูปสามช่อง (id, class, tag)
    (ค) ถ้าย้ายกฎที่ชนะไปไว้บรรทัดบนสุดของไฟล์ ผลจะเปลี่ยนไหม — ตอบก่อน แล้วค่อยลองจริง ถ้าตอบผิดให้กลับไปอ่านหัวข้อ 5 อีกรอบ

ข้อ 3 — ทางเดียวที่เหลือคือ เลิกใช้ #price จัดสไตล์
เปลี่ยน #price { color: gray; } ใน CSS เป็น .price { color: gray; } แล้วไปเติม class นั้นใน HTML เป็น class="price sale-price"
สังเกตว่า id="price" ใน HTML ยังอยู่ครบ ไม่ต้องลบ — เราแค่เลิกให้ CSS ไปยุ่งกับมัน ส่วนลิงก์หรือ JS ที่อาจอ้าง #price อยู่ก็ไม่พัง นี่คือความหมายจริง ๆ ของ "ใช้ id ได้ แต่อย่าเอาไปจัดสไตล์"

(ก) จะเห็น color: gray ของ .price ถูกขีดฆ่าแทน — สลับฝั่งกับตอนแรกพอดี
(ข) ตัวที่ชนะคือ .sale-price น้ำหนัก 0, 1, 0 · ตัวที่แพ้คือ .price ซึ่งก็ 0, 1, 0 เท่ากันเป๊ะ
(ค) เปลี่ยนครับ — และนี่คือหัวใจของภารกิจนี้ พอทั้งคู่เป็น class เท่ากัน ผลจะขึ้นกับลำดับล้วน ๆ ย้าย .sale-price ขึ้นบนสุดเมื่อไหร่ ราคาก็กลับไปเป็นสีเทาทันที
ลองเทียบกับตอนก่อนซ่อมที่ยังเป็น #price — ตอนนั้นย้ายบรรทัดยังไงก็ไม่เปลี่ยน ความต่างของ "ย้ายแล้วเปลี่ยน" กับ "ย้ายแล้วไม่เปลี่ยน" นี่แหละคือสิ่งที่บทนี้อยากให้คุณรู้สึกด้วยมือตัวเอง

ทำจนถึงตรงนี้ได้ แปลว่าคุณดีบัก CSS ที่ "เขียนแล้วไม่เปลี่ยน" ได้ด้วยตัวเองแล้ว ซึ่งเป็นทักษะที่ใช้ยาวกว่าการจำชื่อ property ทุกตัวรวมกัน

💬 ติดตรงไหนถามได้ — โดยเฉพาะสองคำถามนี้ที่คนถามกันบ่อยแล้วบทนี้ยังไม่ได้ตอบ: "แล้วถ้าอยากจับ 'ทุก p ที่อยู่ในกล่องนี้เท่านั้น' ต้องเขียนยังไง" (เขียนสอง selector ติดกันคั่นด้วยเว้นวรรค — เดี๋ยวเจอในบทถัด ๆ ไป) และ "CSS ควรอยู่ในไฟล์แยกหรืออยู่ใน <style> ในหน้าเลย"
เอาโค้ดที่ติดมาถามได้เลย บอกมาว่าคาดว่าจะได้สีอะไร แต่ได้สีอะไรจริง สองอย่างนี้พอสำหรับหาสาเหตุแล้ว

อ้างอิง