🧰 ของที่ใช้ตรวจในบทนี้
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>
Somchai Jaidee
I'm a software developer
สังเกตว่า HTML สองบรรทัดล่างไม่ได้บอกอะไรเรื่องสีหรือขนาดเลย
มันบอกแค่ว่า "อันนี้เป็นหัวข้อใหญ่ อันนี้เป็นย่อหน้า"
ส่วนที่ทำให้หัวข้อกลายเป็นตัวหนังสือ serif สีเขียวขี้ม้าขนาด 48px คือกฎ CSS ข้างบนล้วน ๆ
ลบ <style> ทิ้ง เนื้อหายังอยู่ครบ แค่หน้าตากลับไปเป็นค่าเริ่มต้นของเบราว์เซอร์
2 · หน้าตาของกฎ CSS หนึ่งข้อ — อ่านให้ออกก่อน แล้วที่เหลือง่ายหมด
ก้อนหนึ่งก้อนข้างบนนั้นเรียกว่า กฎหนึ่งข้อ และมันมีแค่สองส่วนเสมอ ไม่มีมากกว่านี้:
ฝั่งซ้ายของปีกกาคือ "จะจัดใคร" ·
ฝั่งในปีกกาคือ "จัดยังไง" เขียนได้หลายคำสั่ง แต่ละคำสั่งลงท้ายด้วย ;
ทั้งบทนี้เราจะสนใจแค่ฝั่งซ้าย เพราะฝั่งขวา (สี ขนาด ระยะห่าง)
ค่อยไล่จำทีหลังก็ได้ ไม่มีอะไรให้เข้าใจผิด แต่ฝั่งซ้ายมีเยอะมาก
นึกภาพครูยืนหน้าเสาธงแล้วประกาศว่า "…… ยกมือขึ้น"
ช่องว่างตรงนั้นคือ ฝั่งซ้ายของปีกกา และครูขานได้หลายแบบ:
· "นักเรียนชายทุกคน" — เรียกตามชนิด ใครเข้าข่ายก็โดนหมด
· "คนที่ติดปลอกแขนสีเหลือง" — เรียกตามป้ายที่เราไปติดให้เอง จะติดกี่คนก็ได้
· "เด็กชายสมชาย เลขที่ 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>
About Me
Product design and web development.
Experience
Dec 2023 - Present
อ่านผลที่ออกมาแล้วจะเห็นความต่างของสามแบบชัดมาก:
h4เป็น tag → หัวข้อ ทั้งสองอัน เปลี่ยนฟอนต์ โดยที่เราไม่ต้องไปแตะ HTML เลยสักตัวอักษร.sub-titleเป็น class → หัวข้อทั้งสองอันโตขึ้นเหมือนกัน เพราะเราไปติดป้ายเดียวกันไว้ให้ทั้งคู่ · ถ้าพรุ่งนี้มี section ที่สาม แค่ติดป้ายเดิมก็ได้หน้าตาเดิมทันที#about-meเป็น id → มีแค่กล่องบนที่พื้นเปลี่ยนสี กล่อง Experience ไม่ขยับเลย แม้จะเป็น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 เปล่า ๆ เช่น
ph1div· ในสเปกทางการเรียก 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>
ผมมีแค่ tag
ผมมี class ด้วย
ผมมีครบทั้งสาม
ผมวัดสีที่ออกมาจริงด้วย 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) | อ่านว่า |
|---|---|---|
h4 | 0, 0, 1 | tag หนึ่งตัว |
.warning | 0, 1, 0 | class หนึ่งตัว |
#top | 1, 0, 0 | id หนึ่งตัว |
h4.warning | 0, 1, 1 | class หนึ่ง + tag หนึ่ง |
.btn.danger | 0, 2, 0 | class สองตัวบน element เดียวกัน |
body div p | 0, 0, 3 | tag สามตัวซ้อนกัน |
วิธีเทียบคือ ดูช่องซ้ายสุดก่อน ใครมากกว่าชนะทันที ไม่ต้องดูช่องขวาเลย ถ้าเท่ากันค่อยขยับไปดูช่องถัดไป
⚠️ กับดัก: สามช่องนี้ไม่ใช่เลขฐานสิบ — 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>
ผมโดน body div p
ผมมี class ชื่อ mark
บรรทัดบนออกมา 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 และมีคนใช้อยู่สามกลุ่ม:
- ลิงก์กระโดดในหน้า —
<a href="#about-me">จะพาไปที่ element ที่ id ตรงกัน · เว็บบทเรียนที่คุณอ่านอยู่นี้ก็ใช้ท่านี้ทำเมนู "ในบทนี้" - ฟอร์ม —
<label for="email">หา<input id="email">ด้วย id (คุณเพิ่งเจอมาในบท 58) · ถ้า id ซ้ำ การกดที่ label แล้วเคอร์เซอร์ไปลงผิดช่องคือเรื่องปกติ - JavaScript —
document.getElementById('about-me')คือทางที่เร็วและตรงที่สุดในการหยิบ 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 · ภารกิจ
ภารกิจนี้ไม่ใช่ "ลองพิมพ์ตาม" แต่คือสร้างอาการเสียขึ้นมาเอง แล้วพิสูจน์ว่าซ่อมได้จริง ด้วยเครื่องมือที่จะอยู่กับคุณไปตลอด คือแท็บ Elements → Styles ใน DevTools
- สร้างอาการเสีย: สร้างไฟล์
selector-lab.htmlที่มีข้อความราคาหนึ่งบรรทัด เขียนเป็น<p id="price" class="sale-price">1,290 บาท</p>
แล้วใน<style>ใส่สองกฎนี้ โดยให้#priceอยู่บรรทัดบน:#price { color: gray; }และ.sale-price { color: red; }
เปิดในเบราว์เซอร์ — ต้องเห็นเป็นสีเทา ถ้าเห็นเป็นสีแดงแปลว่ายังเขียนไม่ตรงโจทย์ ให้ย้อนกลับไปดูว่าพิมพ์#กับ.ครบไหม - พิสูจน์ว่ามันแพ้จริง ไม่ใช่กฎหาย: คลิกขวาที่ข้อความ → Inspect
→ ดูแผง Styles ทางขวา
ต้องเห็นcolor: redถูกขีดฆ่าคาไว้ (ตัวหนังสือมีเส้นพาดกลาง)
นี่คือหลักฐานที่แยกสองอาการที่หน้าตาเหมือนกันออกจากกันได้: ถูกขีดฆ่า = กฎติดแล้วแต่แพ้ · ไม่โผล่มาเลย = selector เขียนไม่ตรง
วันหลังเจอ "เขียน CSS แล้วไม่เปลี่ยน" ให้เช็คตรงนี้เป็นอย่างแรกเสมอ - ซ่อมโดยห้ามใช้ทางลัด: ทำให้ราคาเป็นสีแดงให้ได้ โดยมีข้อห้ามสามข้อ —
ห้ามใช้
!important· ห้ามลบหรือแก้บรรทัด.sale-price· ห้ามเพิ่ม#เข้าไปในไฟล์ CSS อีกตัว
(ใบ้: ข้อจำกัดพวกนี้บีบให้เหลือทางเดียว และเป็นทางที่หัวข้อ 6 บอกไว้แล้ว) - เขียนหลักฐานยืนยัน: 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> ในหน้าเลย"
เอาโค้ดที่ติดมาถามได้เลย บอกมาว่าคาดว่าจะได้สีอะไร แต่ได้สีอะไรจริง
สองอย่างนี้พอสำหรับหาสาเหตุแล้ว
อ้างอิง
- MDN — CSS selectors — อ่านแค่หัวข้อ Basic selectors ก็พอสำหรับบทนี้ จะเห็นสามแบบที่เราเพิ่งเรียน (type · class · id) เรียงอยู่สามบรรทัดแรก ส่วนที่เหลือของหน้าคือของที่จะทยอยเจอในบทหลัง ๆ ยังไม่ต้องอ่านตอนนี้
- MDN — Specificity —
อ่านตาราง Selector weight categories อันเดียว
มันคือที่มาของตัวเลขสามช่อง
(id, class, tag)ในหัวข้อ 4 · ถ้าเลื่อนลงต่อจะเจอหัวข้อ Three-column comparison ซึ่งอธิบายวิธีเทียบจากซ้ายไปขวาแบบเดียวกับที่บทนี้สอน - MDN — Cascade —
อ่านหัวข้อ Order of precedence เพื่อเห็นภาพว่า specificity
เป็นแค่ขั้นตอนหนึ่ง และ source order มาทีหลังจริง ๆ ตามที่บทนี้บอก
· จะได้เห็นด้วยว่า
!importantไปแทรกอยู่ตรงไหนของลำดับ - W3C Selectors Level 4 — §17 Calculating a selector's specificity — ตัวสเปกจริงที่ MDN อ้างอิงอีกที · อ่านแค่ย่อหน้าแรกที่ขึ้นต้นว่า "A selector's specificity is calculated..." จะเห็นคำว่า A, B, C ซึ่งก็คือสามช่องที่บทนี้เรียกว่า (id, class, tag) นั่นเอง — เอาไว้ยืนยันว่าที่เรียนมาไม่ใช่กฎที่ใครคิดเอง
- MDN — the
idattribute — อ่านสองย่อหน้าแรก จะเจอประโยคที่บอกว่าค่าของ id ต้องไม่ซ้ำกันในทั้งเอกสาร และเห็นรายการงานจริงของมัน (ลิงก์ในหน้า ·label· JavaScript) ซึ่งเป็นเหตุผลข้อ 3 ในหัวข้อ 6