🧰 ตรวจด้วยอะไร เวอร์ชันไหน — ทุกตัวเลขและทุกผลรันในหน้านี้ ยกมาจากบทเรียน 56–62 ซึ่งวัดไว้ด้วยของพวกนี้ (รันวันที่ 27 สิงหาคม 2026)
Chrome 148.0.7778.280 (macOS) # บท 58 · 62
Chromium 148 (engine ของ Chrome) # บท 57 — วัดด้วย getComputedStyle() / getBoundingClientRect()
Google Chrome 151.0.7922.174 # บท 59 — วัดสีที่ออกมาจริง
Google Chrome (headless) macOS # บท 60 · 61 — วัดขนาดกล่องจริง
python3 -m http.server 8756 # บท 56 · 58 — static file server ที่ใช้ยิง curl / ดักอ่านฟอร์ม
ตัวเลขที่ขึ้นกับฟอนต์ในเครื่อง (เช่น
39.89 × 20 ของ <span> หรือ 116px / 176px ของ
flex-grow: 1) จะขยับได้นิดหน่อยบนเครื่องคุณ — ที่ต้องตรงคือทิศทาง
ของผล ไม่ใช่ทศนิยม
สารบัญ
- โครงไฟล์ HTML ขั้นต่ำ + วิธีต่อ CSS
- แท็ก HTML ที่ใช้บ่อย — block · inline · semantic
- แท็กฟอร์มครบชุด + attribute ที่ขาดไม่ได้
- selector + ตาราง specificity
- box model — padding · margin · border
- หน่วย px % rem em vh vw
- display — block · inline · inline-block · none
- Flexbox — ตัวไหนใส่ที่แม่ ตัวไหนใส่ที่ลูก
- justify-content / align-items — row เทียบ column
- pseudo-class ของ state + ข้อควรระวัง
- ตารางกับดักรวมทั้งกลุ่ม
- ที่ฝึกต่อ และคู่มือที่ควรรู้ว่ามีอยู่
- อ้างอิง
1 · โครงไฟล์ HTML ขั้นต่ำ + วิธีต่อ CSS
เซฟเป็นไฟล์ .html แล้วดับเบิลคลิกได้เลย ไม่ต้องมี server ไม่ต้อง build (บท 56)
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Joey's Resume</title>
</head>
<body>
<h1>Resume</h1>
<p>Hello, My name is Joey Wasin</p>
</body>
</html>
| บรรทัด | มีไว้ทำอะไร | ลืมแล้วเกิดอะไร |
|---|---|---|
<!DOCTYPE html> | บอกว่า "อ่านไฟล์นี้ด้วยกฎยุคปัจจุบัน" | สลับไป quirks mode · document.compatMode เป็น BackCompat · หน้าดูปกติทุกอย่าง แต่ <td> ไม่รับ font-size ที่สืบทอดมา (ได้ 16px แทน 30px) |
<html lang="th"> | รากของเอกสาร · lang บอกภาษาเนื้อหา | โปรแกรมอ่านหน้าจอออกเสียงผิดภาษา · เบราว์เซอร์เสนอแปลหน้าผิด ๆ |
<head> | ของที่ไม่แสดงบนหน้า — ชื่อแท็บ · การเชื่อม CSS · meta | เบราว์เซอร์สร้างให้เอง · แต่ถ้าเอา <h1> ไปวางก่อน <title> ของที่เหลือตกไปอยู่ใน <body> หมด |
<meta charset="UTF-8"> | บอกวิธีเข้ารหัสตัวอักษร | ภาษาไทยกลายเป็น สวัส… ทั้งหน้า · document.characterSet เดาเป็น windows-1252 |
<meta name="viewport" …> | บอกมือถือให้ใช้ความกว้างจอจริง | เปิดบนมือถือแล้วตัวหนังสือเล็กเท่ามด ต้องซูมเอง |
<title> | ชื่อบนแท็บและในผลค้นหา | แท็บโชว์ชื่อไฟล์หรือ URL ดิบ ๆ แทน |
<body> | ทุกอย่างที่มองเห็นอยู่ในนี้ | เบราว์เซอร์สร้างให้เอง · แต่ไม่มีอะไรข้างในก็ไม่มีอะไรให้ดู |
เอา CSS มาต่อกับ HTML — มีสามทาง ใช้จริงทางเดียว
| วิธี | เขียนยังไง | ใช้เมื่อไหร่ |
|---|---|---|
| ไฟล์แยก (external) | <link rel="stylesheet" href="style.css"> ใน <head> | ค่าเริ่มต้นที่ควรใช้เสมอ — ไฟล์เดียวคุมได้ทุกหน้า และเบราว์เซอร์แคชไว้ใช้ซ้ำได้ |
| ฝังในไฟล์ (internal) | <style> … </style> ใน <head> | ไฟล์เดียวจบ เช่นตัวอย่างสอน · หน้าเดียวโดด ๆ · อีเมล HTML |
| ติดที่ตัว element (inline) | <p style="color: red"> | เกือบไม่ควรใช้ · มันชนะกฎกลางเสมอ (ยกเว้น !important) แก้แล้วต้องไล่หาทีละจุด |
นับ request ให้เป็น
หนึ่งหน้าเว็บ ≠ หนึ่ง request · เบราว์เซอร์ยิงเพิ่มเองทุกครั้งที่เจอ href / src
ชี้ไปหาไฟล์อื่น — หน้าที่อ้าง CSS 1 · รูป 3 · สคริปต์ 2 = 7 request
(ตัวหน้า HTML เองนับด้วย)
2 · แท็ก HTML ที่ใช้บ่อย — block · inline · semantic
เส้นแบ่งเส้นเดียวที่อธิบายได้ทั้งหมด: block กินทั้งบรรทัดและดันตัวถัดไปลงบรรทัดใหม่ ·
inline แทรกในบรรทัดและกว้างเท่าเนื้อข้างใน (วัดจริงในหน้ากว้าง 482px:
div ได้ 482px ทั้งคู่คนละแถว · span ได้ 95px กับ 94px แถวเดียวกัน)
| แท็ก | พันธุ์ | ใช้ตอนไหน | จุดพลาดที่เจอบ่อยที่สุด |
|---|---|---|---|
<div> | block | มัดของเป็นก้อน เมื่อไม่มีแท็กที่มีความหมายตรงกว่า | ใส่ซ้อนกันโดยที่ยังตอบไม่ได้ว่าจะแปะ class อะไร |
<p> | block | ย่อหน้าข้อความ | เอา block ไปใส่ข้างใน แล้วเบราว์เซอร์แอบปิด </p> ให้ |
<h1>–<h6> | block | หัวข้อ · เลือกระดับตามโครงเรื่อง ไม่ใช่ตามขนาดตัวอักษร | เลือกตามขนาดแล้วข้ามระดับ (h2 → h4) |
<span> | inline | จับคำกลางประโยคเพื่อจัดสไตล์ | สั่ง width/height แล้วงงว่าไม่มีผล (ได้ 118×19 ไม่ใช่ 120×60) |
<header> | block | ส่วนหัวของหน้า (หรือของบทความ) | ใช้ <div> แทนทั้งที่ตัวจริงมีอยู่ — หน้าตาไม่ต่างสักพิกเซล (วัดได้ 482px · display: block เหมือนกันหมดทั้งสี่) ประโยชน์อยู่ที่ landmark ของ screen reader · Google · และคนที่มาแก้โค้ดต่อ |
<main> | block | เนื้อหาหลัก · หน้าละหนึ่งอัน | |
<footer> | block | ท้ายหน้า · ลิขสิทธิ์ · ลิงก์ติดต่อ | |
<img> | inline | ใส่รูป · เป็น void element ไม่มีแท็กปิด | ลืม alt หรือใส่ alt เป็นชื่อไฟล์ |
<a> · <strong> | inline | ลิงก์ · เน้นข้อความ | เหมือน <span> — เมิน width/height |
ขนาดเริ่มต้นจริงของหัวข้อ (วัดในหน้าเปล่า ไม่มี CSS เลย · ข้อความปกติ = 16px)
h1 | h2 | h3 | h4 | h5 | h6 |
|---|---|---|---|---|---|
| 32px | 24px | 18.72px | 16px | 13.28px | 10.72px |
| 2 เท่า | 1.5 เท่า | 1.17 เท่า | เท่ากันพอดี | เล็กกว่า | เล็กกว่ามาก |
alt="" กับ "ไม่ใส่ alt" ไม่ใช่สิ่งเดียวกัน
ตาแยกไม่ออก แต่ screen reader แยกออก — ทดสอบด้วย <img> ที่ชี้ไปไฟล์ที่ไม่มีจริง สามอัน:
| เขียนแบบ | ตาเห็น | screen reader ได้ยิน |
|---|---|---|
alt="แมวส้มนอนบนคีย์บอร์ด" | ข้อความนั้นแทนรูป · กล่องหดเหลือ 171×26 ไม่ใช่ 200×60 ตามที่สั่ง | ข้อความนั้น |
alt="" | กล่องเปล่า 200×60 ตามที่สั่ง | ข้ามเงียบ ๆ — ถูกต้องสำหรับรูปประดับ |
ไม่ใส่ alt | กล่องเปล่า 200×60 — เหมือนอันบนเป๊ะ | ไม่รู้ว่าควรข้ามไหม ส่วนใหญ่เลยอ่านชื่อไฟล์ออกมาดัง ๆ |
คำถามที่ใช้เขียน alt ให้ถูกทุกครั้ง: "ถ้ารูปนี้หายไป ผู้อ่านเสียข้อมูลอะไร"
— เสีย → เขียนข้อมูลนั้นลงไป · ไม่เสียอะไร (ข้อความข้าง ๆ บอกอยู่แล้ว) → alt="" ·
ทุก <img> ต้องมี alt เสมอ คำถามมีแค่ว่าจะใส่ว่าอะไร
✗ p ครอบ div
<p>ก่อน<div>กลาง</div>หลัง</p>
<!-- DOM ที่ได้จริง (อ่านจาก innerHTML): -->
<p>ก่อน</p><div>กลาง</div>หลัง<p></p>
ได้กรอบ p สองอันจากที่พิมพ์อันเดียว · ไม่มี error ไม่มีคำเตือน
✓ div ครอบ p
<div>
<p>…</p>
</div>
DOM ที่ได้ตรงกับที่พิมพ์ทุกตัวอักษร
HTML entity
< ได้ < · > ได้ > ·
& ได้ & · © ได้ ©
— ต้องใช้เวลาอยากโชว์โค้ด HTML บนหน้าเว็บ ไม่งั้นเบราว์เซอร์นึกว่าเป็นแท็กจริง
3 · แท็กฟอร์มครบชุด + attribute ที่ขาดไม่ได้
| แท็ก | ทำอะไร | attribute ที่ขาดไม่ได้ |
|---|---|---|
<form> | "ซอง" ที่ลากเส้นล้อมของไว้ — ไม่มีหน้าตาของตัวเอง | action (URL ปลายทาง · ไม่ใส่ = ส่งกลับมาที่หน้าตัวเอง) · method (GET/POST เท่านั้น · ไม่ใส่ = GET) |
<input> | ช่องกรอกบรรทัดเดียว · void element ไม่มีแท็กปิด | type · id (ให้ label เกาะ) · name (ไม่มี = ไม่ถูกส่ง) · required · placeholder · value |
<label> | ข้อความกำกับหน้าช่อง + ผูกตัวเองเข้ากับช่อง | for ต้องตรงกับ id ทุกตัวอักษร (หรือเอา <input> ไปไว้ข้างใน <label> เลย ไม่ต้องมี for/id) |
<button> | ปุ่ม | type — เขียนทุกปุ่มเสมอ ไม่ใช่เพราะเบราว์เซอร์ต้องการ แต่เพราะคนอ่านโค้ดต่อต้องการ |
<textarea> | พิมพ์ได้หลายบรรทัด · ลากมุมขยายได้ | id · name · rows · ไม่มี value — ค่าเริ่มต้นคือของที่อยู่ระหว่างแท็ก จึงควรเขียน </textarea> ปิดติดกันไปเลย |
<select> | เลือกจากรายการที่กำหนดไว้ พิมพ์เองไม่ได้ | id · name |
<option> | หนึ่งตัวเลือกใน <select> | value — ใส่ให้ทุกอันเสมอ (ไม่ใส่ = ส่งข้อความข้างในไปแทน วันไหนมีคนแก้คำภาษาไทย ค่าใน database เปลี่ยนตาม) · disabled selected ใช้ทำ "หัวข้อหลอก" |
type ของ <input> ที่ใช้บ่อย
type | ได้อะไรเพิ่มจาก text | เหมาะกับ |
|---|---|---|
text | ค่าเริ่มต้น ไม่มีอะไรพิเศษ | ชื่อ · ที่อยู่ · อะไรก็ได้ |
email | เบราว์เซอร์ตรวจรูปแบบให้ก่อนส่ง · มือถือขึ้นคีย์บอร์ดที่มี @ | อีเมล |
password | ปิดบังตัวอักษรเป็นจุด | รหัสผ่าน |
number | มีลูกศรเพิ่ม/ลด · มือถือขึ้นคีย์บอร์ดตัวเลข | อายุ · จำนวน |
checkbox | กลายเป็นช่องติ๊ก · ส่งค่าก็ต่อเมื่อถูกติ๊ก | ยอมรับเงื่อนไข |
radio | เลือกได้อันเดียวในกลุ่มที่ name เหมือนกัน | เพศ · ตัวเลือกไม่กี่อัน |
type ของ <button> — ในฟอร์ม ถ้าไม่ใส่ เบราว์เซอร์ถือว่าเป็น
submit ให้อัตโนมัติ (อ่าน button.type จาก DOM ได้ "submit" ทั้งที่ไม่ได้เขียนไว้)
type | กดแล้วเกิดอะไร | ใช้ตอนไหน |
|---|---|---|
submit (ค่าเริ่มต้น) | รวบค่าทั้งฟอร์มส่งไป action แล้วโหลดหน้าใหม่ | ปุ่มส่งจริง ๆ ปุ่มเดียวของฟอร์ม |
button | ไม่ทำอะไรเลย รอให้ JavaScript มาผูกงานให้ | ปุ่มอื่นทุกปุ่ม — ล้างช่อง · เพิ่มแถว · เปิด popup |
reset | คืนทุกช่องกลับเป็นค่าตั้งต้น (ไม่ใช่ค่าว่าง) | แทบไม่ใช้แล้ว เพราะคนกดพลาดบ่อย |
id กับ name — คนละหน้าที่กันคนละเรื่อง
id | name | |
|---|---|---|
| ใครใช้ | <label for> · CSS · JavaScript | ตัวเบราว์เซอร์ตอนส่ง และ handler ฝั่ง Go |
| ต้องไม่ซ้ำไหม | ต้องไม่ซ้ำทั้งหน้า | ซ้ำได้ — radio กลุ่มเดียวกันต้องซ้ำด้วยซ้ำ |
| ไม่ใส่แล้วเป็นไง | คลิก label ไม่โฟกัส · JS หาไม่เจอ | ค่าไม่ถูกส่งไปเลย — เงียบสนิท ไม่มี warning |
| ตั้งชื่อยังไงดี | ตั้งให้อ่านง่ายในหน้านี้ | ตั้งให้ตรงกับ tag form:"…" ฝั่ง Go |
ฟอร์มไปโผล่ตรงไหนในโค้ด Go — ฟอร์ม method="POST" ส่ง
Content-Type: application/x-www-form-urlencoded ไม่ใช่ JSON
(ดักอ่าน request จริงแล้วได้ BODY=fullname=%E0%B8%AA…&topic=consult&message=…)
| ในฟอร์ม HTML | ฝั่ง Gin หยิบด้วย | หรือผูกเข้า struct ด้วย tag |
|---|---|---|
<input name="fullname"> ในฟอร์ม POST | c.PostForm("fullname") | form:"fullname" + c.ShouldBind(&req) |
<input name="q"> ในฟอร์ม GET | c.Query("q") | form:"q" + c.ShouldBindQuery(&req) |
<select name="topic"> | c.PostForm("topic") | form:"topic" — ได้ value ของ option ที่เลือก |
<textarea name="message"> | c.PostForm("message") | form:"message" — เหมือน input ทุกอย่าง |
type="email" ตรวจให้จริง (ใส่ค่าที่ไม่ใช่อีเมลแล้วกดส่ง
ได้ Please include an '@' in the email address. และ event submit
ไม่ถูกยิงออกมาด้วยซ้ำ) — แต่ใครก็ยิง curl ตรงเข้า endpoint ข้ามหน้าเว็บได้
ตัวที่กันของเสียจริงคือ binding:"required,email" ฝั่ง Go ·
และฟอร์ม HTML ล้วน ๆ ส่ง PUT/DELETE ไม่ได้ — ใส่ไปก็ตกกลับไปใช้ GET เงียบ ๆ
4 · selector + ตาราง specificity
กฎ CSS หนึ่งข้อมีสองส่วนเสมอ: ฝั่งซ้ายของปีกกา = "จะจัดใคร" ·
ฝั่งในปีกกา = "จัดยังไง" (คู่ property: value; หนึ่งคู่ = หนึ่ง declaration)
| แบบ | เขียนยังไง | จับอะไร | ในหน้าหนึ่งมีได้กี่ตัว |
|---|---|---|---|
| tag (type selector) | h1 { } — ชื่อ element เปล่า ๆ ไม่มีสัญลักษณ์นำหน้า | ทุก element ที่เป็นชนิดนั้น ทั้งหน้า | กี่ตัวก็ได้ — ไม่ได้เลือก มันจับหมด |
| class | .sub-title { } — นำหน้าด้วยจุด | ทุก element ที่เขียน class="sub-title" ไว้ | กี่ตัวก็ได้ · หนึ่ง element ติดได้หลาย class (class="btn danger") |
| id | #about-me { } — นำหน้าด้วยชาร์ป | element ตัวเดียวที่เขียน id="about-me" | ชื่อหนึ่ง ๆ ห้ามซ้ำในหน้าเดียวกัน |
specificity — น้ำหนักสามช่อง (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 สามตัวซ้อนกัน |
.btn:hover เทียบ .btn:active | เท่ากันเป๊ะ | class หนึ่งตัว + pseudo-class หนึ่งตัว ทั้งคู่ → ตัดสินด้วยลำดับ |
เทียบจากช่องซ้ายสุด ใครมากกว่าชนะทันที ช่องขวาไม่มีสิทธิ์แซง —
.mark (0,1,0) ชนะ body div p (0,0,3) ทั้งที่ tag มีตั้งสามตัวและเขียนทีหลัง
(วัดสีจริงได้ rgb(0, 128, 0) = green · ลองยืดเป็น tag สิบเอ็ดตัวซ้อนกันก็ยังเขียว)
ลำดับการตัดสิน — จำผิดแล้วจะไปนั่งสลับบรรทัดทั้งวันโดยหน้าไม่เปลี่ยนเลย
- ดูน้ำหนักก่อนเสมอ —
idชนะclassชนะtag(วัดจริง: tag เปล่า = teal · +class = orange · +id = crimson) · ไม่เกี่ยวกับใครเขียนก่อนหลังเลย - เสมอกันเมื่อไหร่ ค่อยดูว่าใครเขียนทีหลัง (source order) — บรรทัดล่างชนะ
| สิ่งที่อยากทำ | ใช้อะไร | เพราะอะไร |
|---|---|---|
| ตั้งฟอนต์ · สีตัวหนังสือ · ขนาดพื้นฐานของทั้งหน้า | tag เช่น body { } | อยากให้โดนทุกตัวจริง ๆ และน้ำหนักต่ำสุดพอดี ทับทีหลังได้ง่าย |
| ปุ่ม · การ์ด · ป้ายสถานะ · อะไรก็ตามที่มีมากกว่าหนึ่งอัน | class ← ค่าเริ่มต้นของงานส่วนใหญ่ | ใช้ซ้ำได้ · น้ำหนักเท่ากันหมดจึงเดาผลได้จากลำดับ · ถอดออกก็ง่าย |
| จุดเดียวในหน้าที่หน้าตาไม่เหมือนใครเลย | class ที่ตั้งชื่อเฉพาะ เช่น .hero-title | ได้ความเจาะจงที่ต้องการโดยไม่ต้องรับน้ำหนักของ id มาด้วย |
| ลิงก์กระโดดไปหัวข้อ · ผูก label กับช่องกรอก · ให้ JS หยิบ element | id | นี่คืองานที่ id ถูกออกแบบมาเพื่อมัน และไม่มีอะไรทำแทนได้ |
เขียน id ใน HTML ได้เต็มที่ —
แต่ในไฟล์ CSS อย่าให้มี # โผล่มาเลย
วันไหนรู้สึกว่า "ต้องใช้ # แล้วล่ะ ไม่งั้นสู้ไม่ได้" นั่นคือสัญญาณว่ามีกฎข้ออื่นแรงเกินไปอยู่ก่อนแล้ว
— ให้ไปลดน้ำหนักกฎนั้น อย่าเพิ่มน้ำหนักฝั่งเรา
5 · box model — padding · margin · border
| ชั้น | คืออะไร | พื้นหลังทาถึงไหม |
|---|---|---|
content | ตัวข้อความ รูป หรือกล่องลูกที่อยู่ข้างใน | ทา |
padding | ที่ว่างด้านใน ระหว่างเนื้อหากับขอบกล่อง | ทา |
border | เส้นกรอบ — มีความหนาจริง กินที่จริง | เป็นเส้นเอง |
margin | ที่ว่างด้านนอก ระหว่างกล่องนี้กับกล่องอื่น | ไม่ทา — โปร่งใสเสมอ |
ช่องขวาสุดคือวิธีดูด้วยตาเปล่า: ทาสีพื้นให้กล่องแล้วดูว่าสีขยายตามไปด้วยไหม —
ขยาย = padding · ไม่ขยาย = margin
เขียนค่าได้กี่แบบ — padding กับ margin ใช้กติกาเดียวกันเป๊ะ จำครั้งเดียวได้ทั้งคู่
| เขียนแบบ | ตัวอย่าง | แปลว่า |
|---|---|---|
| 1 ค่า | padding: 64px; | ทั้งสี่ด้าน 64 |
| 2 ค่า | padding: 120px 64px; | บน/ล่าง 120 · ซ้าย/ขวา 64 — แบบที่เจอบ่อยที่สุดในโค้ดจริง |
| 4 ค่า | padding: 120px 64px 120px 64px; | บน · ขวา · ล่าง · ซ้าย (วนตามเข็มนาฬิกา) |
| แยกด้าน | padding-top: 120px; | ระบุทีละด้าน มีครบสี่ตัว -top -right -bottom -left |
margin มีของแถมที่ padding ไม่มีสองอย่าง: รับค่าติดลบได้
(ดึงกล่องให้ทับกัน) และ รับคำว่า auto — margin: 0 auto;
คือวิธีจัดกล่องให้อยู่กลางแนวนอน
border เป็น shorthand รวมสามค่า: ความหนา · รูปแบบ · สี ·
ไม่ใส่ style เส้นจะไม่ขึ้นเลย เพราะค่าเริ่มต้นของ border-style
คือ none — เขียน border: 4px red; อย่างเดียวจึงไม่เห็นอะไรทั้งนั้น
border: 4px solid #495057; /* shorthand — ใช้แบบนี้เกือบตลอด */
border-width: 4px; /* เขียนแยกเมื่อต้องแก้เฉพาะบางส่วนทีหลัง */
border-style: solid; /* เช่น border-color: red; ตอน hover */
border-color: #1971c2;
border-radius: 12px;
width: 100% แล้วล้น
กรอบพ่อ 300px · ลูกสั่ง width: 100%; padding: 20px; border: 4px →
วัด getBoundingClientRect() ได้ 348px ไม่ใช่ 300
300 (content) + 20×2 (padding) + 4×2 (border) = 348 /* content-box = ค่าเริ่มต้น */
box-sizing: border-box; /* → ได้ 300 พอดี · content หดเหลือ 252 */
border-box ไม่ได้ทำให้ padding หายไป — มันเบียดที่ของ content แทน ·
ไม่มี error ไม่มี warning · รู้ตัวตอนเห็น scrollbar แนวนอนโผล่มาเท่านั้น ·
นี่คือเหตุผลที่โปรเจกต์จริงเกือบทุกโปรเจกต์เขียน * { box-sizing: border-box; }
ไว้บรรทัดแรกของไฟล์ CSS แล้วไม่ต้องคิดเรื่องนี้อีกเลย
กล่องบน margin-bottom: 40px · กล่องล่าง margin-top: 24px →
ช่องว่างจริง 40px ไม่ใช่ 64
margin แนวตั้งของสองกล่องที่ติดกันไม่บวกกัน — มันยุบรวมเป็นก้อนเดียวแล้วเอา
max(40, 24) = 40 · แก้ 24 เป็น 30 ก็ยังได้ 40 (ทุกค่าที่น้อยกว่า 40 ให้ผลเหมือนกันหมด)
เกิดเฉพาะแนวตั้ง ไม่เกิดแนวนอน · และยัง "ทะลุ" ออกจากกล่องพ่อได้ด้วย —
พ่อ div เปล่า ๆ ที่ลูกสั่ง margin-top: 30px → ขอบบนของพ่ออยู่ตำแหน่งเดียวกับลูกเป๊ะ
และพ่อสูงแค่ 18px · เติม padding-top: 1px ให้พ่อ ระยะกลายเป็น 31px ทันที
— 1 pixel เดียวหยุดการยุบได้
ทางออกที่คนสมัยนี้ใช้: ทำพ่อเป็น
display: flex แล้วใช้ gap — ข้างในกล่อง flex ไม่มีการยุบรวม
(ชุดเดิมเป๊ะ พอสั่ง display: flex; flex-direction: column; ระยะกลายเป็น 64px ทันที)
6 · หน่วย px % rem em vh vw
| หน่วย | อ้างอิงจาก | ใช้ตอนไหน |
|---|---|---|
px | ไม่อ้างอิงอะไร — เป็นค่าคงที่ | ความหนาเส้น · ระยะเล็ก ๆ ที่ไม่ควรขยับ |
% | ขนาดของกล่องพ่อ | ความกว้างที่ต้องยืดตามพ่อ |
rem | font-size ของ <html> (ปกติ 16px) | ค่าเริ่มต้นที่ควรใช้ สำหรับ font-size และระยะห่าง |
em | font-size ของตัวมันเอง — ยกเว้นตอนใช้กับ font-size ที่จะอ้างจากพ่อ | ระยะที่ต้องโตตามตัวอักษร เช่น padding ของปุ่ม |
vw | 1% ของความกว้างหน้าจอ | ของที่ต้องเต็มจอ เช่น hero banner |
vh | 1% ของความสูงหน้าจอ | section สูงเต็มจอ (100vh) |
| โจทย์ | ค่าที่วัดได้จริง | เพราะ |
|---|---|---|
พ่อ 20px → ลูก 1.5em → ลูกของลูก 1.5em | 20px → 30px → 45px | em ทบต้น — ชั้นสองคือ 1.5 ของ 30 ไม่ใช่ 1.5 ของ 20 · ในไฟล์ CSS เห็นแค่ 1.5em เท่ากันทุกชั้น ต้องเปิด DevTools อ่าน computed value ถึงจะรู้ |
ในโครงเดียวกัน ลูกสั่ง 1.5rem | 24px เสมอ (16 × 1.5) | rem อ้างจากรากเดียวเสมอ ซ้อนกี่ชั้นก็ไม่เปลี่ยน |
ลูก font-size: 32px ในพ่อ font-size: 20px สั่ง padding: 1em | 32px ไม่ใช่ 20px | em อ้างจากตัวมันเอง — คำว่า "อ้างจากพ่อ" ถูกเฉพาะตอนใช้กับ font-size เท่านั้น |
พ่อกว้าง 200 สูง 300 · ลูกสั่ง padding: 10% | 20px ทั้งบนและซ้าย (ไม่ใช่ 30px ด้านบน) | % ของ padding กับ margin คิดจากความกว้างของพ่อเสมอ ทุกด้าน — เป็นเหตุผลที่คนไม่ค่อยใช้ % กับระยะห่าง |
จำง่าย ๆ: em = "เท่ากับตัวหนังสือของกล่องนี้" ·
ระยะห่างและขนาดตัวอักษรทั่วไปใช้ rem เก็บ em
ไว้ใช้กับของที่อยากให้โตตามตัวหนังสือของมันเอง
font-family · background — สองข้อที่ควรรู้
font-familyเขียนเป็นรายการสำรองคั่นด้วยจุลภาค เบราว์เซอร์ไล่จากซ้ายไปขวา ตัวไหนมีในเครื่องก็ใช้ตัวนั้น — ตัวสุดท้ายควรเป็นชื่อกลุ่มอย่างsans-serifไว้กันเหนียว · ชื่อฟอนต์ที่มีเว้นวรรคต้องใส่เครื่องหมายคำพูดbackground-colorกับbackground-imageอยู่คนละชั้น ไม่ได้แทนที่กัน — image อยู่บน color อยู่ล่าง · ถ้ารูปโหลดไม่ขึ้นหรือมีส่วนโปร่งใส จะเห็นสีข้างล่างโผล่มา ใส่ทั้งคู่จึงเป็นเรื่องปกติ ไม่ใช่เขียนซ้ำซ้อน ·linear-gradient()นับเป็น "รูป" เต็มตัวในสายตา CSS ใช้กับbackground-imageได้ตรง ๆ โดยไม่ต้องโหลดไฟล์
7 · display — block · inline · inline-block · none
| ค่า | วางตัวยังไง | width/height ใช้ได้ไหม | ใครเป็นแบบนี้อยู่แล้ว |
|---|---|---|---|
block | ขึ้นบรรทัดใหม่เสมอ · กว้างเต็มพ่อ | ได้ | div · p · h1 · header · main · footer |
inline | ไหลไปกับข้อความ ไม่ขึ้นบรรทัดใหม่ | ไม่ได้ — โดนเมินเงียบ ๆ | span · a · strong · img |
inline-block | ไหลไปกับข้อความ แต่ตั้งขนาดได้ | ได้ | button · input · select · textarea เป็นมาแต่กำเนิด (อ่านจาก getComputedStyle().display) · ที่ต้องสั่งเองคือ div กับ span |
none | หายไปจากหน้าเว็บ ไม่กินที่เลย | ไม่มีความหมาย | ไม่มี |
วัดจริงกับ <span> ที่สั่ง width: 120px; height: 40px เหมือนกันเป๊ะ:
inline ได้ 39.89 × 20 (เท่าข้อความ บวก border 1px สองข้าง — ทั้ง width และ height ถูกเมิน) ·
inline-block ได้ 122 × 42 (120 × 40 ตามสั่ง บวก border 1px สองข้าง) ·
block เด้งลงบรรทัดใหม่ · none ไม่มีกล่องที่สี่เลย
display: none ต่างจาก "ซ่อนไว้"
ไม่กินที่แม้แต่ pixel เดียว (กล่องสูง 30px สามใบ ใบกลางสั่ง none
→ รวมสูง 60px ไม่ใช่ 90px) และโปรแกรมอ่านหน้าจอก็อ่านไม่เจอด้วย ·
อยากซ่อนแค่ทางสายตาแต่ยังกินที่เดิม → visibility: hidden;
8 · Flexbox — ตัวไหนใส่ที่แม่ ตัวไหนใส่ที่ลูก
ใส่ผิดที่ไม่ error แต่จะไม่มีอะไรเกิดขึ้นเลย —
และ display: flex ใส่ที่กล่องแม่ แล้วมันไปเปลี่ยนพฤติกรรมของลูก
ไม่ใช่ของตัวมันเอง
| ใส่ที่ กล่องแม่ (flex container) | ทำอะไร | ใส่ที่ กล่องลูก (flex item) | ทำอะไร |
|---|---|---|---|
display: flex | เปิดโหมด flex — ลูกชั้นแรกเรียงต่อกัน | flex-grow | ถ้ามีที่ว่างเหลือ ขอรับไปกี่ส่วน (เริ่มต้น 0) |
flex-direction | ย้ายแกนหลัก | flex-shrink | ถ้าที่ไม่พอ ยอมหดกี่ส่วน (เริ่มต้น 1) |
flex-wrap | ยอมขึ้นบรรทัดใหม่ไหม | flex-basis | ออกตัวที่ขนาดเท่าไหร่ ก่อนแบ่งที่ว่าง (เริ่มต้น auto) |
justify-content | จัดตำแหน่งตามแกนหลัก | flex | รูปย่อของสามตัวบน |
align-items | จัดตำแหน่งตามแกนรอง | align-self | แหกค่าของแม่เฉพาะตัวนี้ |
gap · row-gap · column-gap | ระยะห่างระหว่างลูก | order | สลับลำดับที่วาด |
แกนหลัก (main axis) กับแกนรอง (cross axis) — ไม่ใช่ "แนวนอน" กับ "แนวตั้ง" แต่เป็น "ตามแถว" กับ "ขวางแถว"
flex-direction | แกนหลักชี้ไปทาง | ใช้ตอนไหน |
|---|---|---|
row (ค่าเริ่มต้น) | ซ้าย → ขวา | navbar · แถวปุ่ม · แถวการ์ด |
row-reverse | ขวา → ซ้าย | อยากให้ของชิ้นแรกใน HTML ไปโผล่ขวาสุด |
column | บน → ล่าง | ฟอร์ม · sidebar · การ์ดที่มีหัว-เนื้อ-ท้าย |
column-reverse | ล่าง → บน | กล่องแชตที่ข้อความใหม่อยู่ล่างสุด |
reverse สลับแค่ที่ตา ไม่ได้สลับที่หู
ลำดับใน HTML ยังเหมือนเดิม — screen reader และการกด Tab
ยังไล่ตามลำดับใน HTML อยู่ดี · คนที่ใช้คีย์บอร์ดจะเห็น focus กระโดดจากขวาไปซ้ายแบบงง ๆ ·
ถ้าลำดับสำคัญ ให้แก้ที่ HTML ไม่ใช่ที่ CSS
gap — สั่งครั้งเดียวที่แม่ ได้ช่องว่างระหว่างกล่องเท่านั้น ขอบนอกไม่โดน
| เขียนแบบนี้ | เท่ากับ | อ่านว่า |
|---|---|---|
gap: 8px; | row-gap: 8px; column-gap: 8px; | ห่างเท่ากันทุกทิศ |
gap: 8px 16px; | row-gap: 8px; column-gap: 16px; | แถวก่อน คอลัมน์ทีหลัง — ห่างระหว่างบรรทัด 8 · ระหว่างกล่องในบรรทัด 16 |
row-gap: 8px; | — | ช่องว่างที่คั่นระหว่างแถว ซึ่งวัดกันในแนวตั้ง (เมื่อ row) — ชื่อมันหลอก |
column-gap: 16px; | — | ระยะห่างระหว่างกล่องในบรรทัดเดียวกัน |
flex-wrap — ค่าเริ่มต้นคือ nowrap แปลว่า
"ยัดลงหนึ่งบรรทัดให้ได้ ไม่ว่าจะต้องบีบแค่ไหน"
/* แม่กว้าง 260px · gap 8px · ลูก 4 กล่อง กล่องละ width: 100px */
nowrap (ค่าเริ่มต้น) → วัดได้กล่องละ 59px // (260 − 8×3) ÷ 4 = 59
wrap → ได้ 100px ตามที่สั่ง // กล่องที่ไม่พอตกไปบรรทัดใหม่
flex ย่อมาจากอะไร — ตัวที่เซอร์ไพรส์คนมากที่สุดคือ flex-basis: 0%
ที่แอบมากับ flex: 1
| เขียนย่อ | เท่ากับเขียนเต็ม | แปลว่า |
|---|---|---|
flex: 1; | flex-grow: 1; flex-shrink: 1; flex-basis: 0%; | ลืมขนาดเนื้อหาไปเลย แล้วหารที่ว่างทั้งกล่องเท่า ๆ กัน |
flex: auto; | flex-grow: 1; flex-shrink: 1; flex-basis: auto; | เริ่มจากขนาดเนื้อหา แล้วค่อยแบ่งที่ว่างที่เหลือ |
flex: none; | flex-grow: 0; flex-shrink: 0; flex-basis: auto; | ขนาดเท่าเนื้อหาตายตัว ไม่โต ไม่หด |
flex: 0 1 auto; | — | ค่าเริ่มต้นของทุก flex item ที่ไม่ได้สั่งอะไรเลย |
วัดจริง — แม่กว้าง 300px · gap: 8px · ลูกสองกล่องเนื้อหายาวไม่เท่ากัน:
| เขียนแบบ | กล่อง "สั้น" | กล่อง "ยาวกว่ามาก" |
|---|---|---|
flex-grow: 1 | 116px | 176px |
flex: 1 | 146px | 146px — คือ (300 − 8) ÷ 2 · ไม่ขึ้นกับเนื้อหาเลย |
อยากได้ "แบ่งเท่ากัน" → flex: 1 ·
อยากได้ "เนื้อหาสั้นก็แคบหน่อย" → flex: auto หรือ flex-grow: 1 เฉย ๆ
navbar ของจริง — ทั้งเรื่องมาบรรจบกันที่ของชิ้นเดียวที่เห็นบนเว็บทุกเว็บ
/* วิธีที่ 1 — ตัวคั่นเปล่า ๆ */
.bar { display: flex; align-items: center; gap: 12px; }
.spacer { flex: 1; } /* พองจนเต็มช่องว่าง แล้วดันทุกอย่างหลังมันไปชิดขวา */
<div class="bar">
<span class="brand">PEA DevPool</span>
<a href="#">บทเรียน</a>
<span class="spacer"></span>
<a href="#">เข้าสู่ระบบ</a>
</div>
/* วิธีที่ 2 — ไม่เพิ่ม element เลย · ดีกว่าในแง่ accessibility */
.login { margin-left: auto; } /* margin: auto ใน flexbox ดูดที่ว่างที่เหลือมาเป็นของตัวเอง */
/* วิธีที่ 3 — ห่อสามอันแรกไว้ใน div เดียว แล้วใส่ที่แม่ */
.bar { justify-content: space-between; } /* เหมาะเมื่อมี "กลุ่มซ้าย" กับ "กลุ่มขวา" ชัดเจน */
9 · justify-content / align-items — row เทียบ column
อย่าจำว่า "justify = แนวนอน" ให้จำว่า
"justify = ตามแถว" แล้วถามต่อทุกครั้งว่า "แถวนี้มันเรียงไปทางไหน"
พิสูจน์แล้วด้วยกล่องสองกล่องที่ CSS เหมือนกันเป๊ะ (justify-content: flex-end; align-items: center;)
ต่างกันแค่กล่องขวาเติม flex-direction: column หนึ่งบรรทัด —
กล่องซ้ายได้ ชิดขวา + กลางแนวตั้ง ส่วนกล่องขวาได้ ชิดล่าง + กลางแนวนอน
| เขียนอะไร | row (ค่าเริ่มต้น) — แกนหลักชี้ขวา | column — แกนหลักชี้ลง |
|---|---|---|
justify-content: flex-start (เริ่มต้น) | ชิดซ้าย | ชิดบน |
justify-content: flex-end | ชิดขวา | ชิดล่าง |
justify-content: center | กลางแนวนอน | กลางแนวตั้ง |
align-items: flex-start | ชิดบน | ชิดซ้าย |
align-items: flex-end | ชิดล่าง | ชิดขวา |
align-items: center | กลางแนวตั้ง | กลางแนวนอน |
align-items: stretch (เริ่มต้น) | ลูกยืดเต็มความสูงของแม่เอง | ลูกยืดเต็มความกว้างของแม่เอง |
นี่คือคำตอบของคำถามที่คนถามกันมากที่สุดในโลก CSS — จัดกลางทั้งสองแกน
คือ display: flex; justify-content: center; align-items: center;
ซึ่งใช้ได้เหมือนกันทั้ง row และ column เพราะ center ของทั้งสองแกนคือจุดเดียวกัน
ค่าที่เหลือของ justify-content — "ที่ว่างที่เหลือไปกองอยู่ตรงไหน"
| ค่า | ที่ว่างไปกองอยู่ตรงไหน |
|---|---|
flex-start (ค่าเริ่มต้น) | ท้ายแถวทั้งหมด — ของจึงชิดหัวแถว |
flex-end | หัวแถวทั้งหมด — ของจึงชิดท้ายแถว |
center | แบ่งครึ่งไปหัวแถวกับท้ายแถวเท่ากัน |
space-between | แทรกอยู่ระหว่างของเท่านั้น หัวกับท้ายไม่มีเลย |
space-around | ทุกกล่องได้ที่ว่างรอบตัวเท่ากัน → ขอบนอกจึงบางกว่าช่องกลางครึ่งหนึ่ง |
space-evenly | ทุกช่องเท่ากันหมด รวมช่องริมสองข้าง |
ค่าที่เหลือของ align-items: stretch (ค่าเริ่มต้น) ·
flex-start · flex-end · center ·
baseline (เส้นฐานตัวอักษรตรงกัน ไม่ใช่ขอบกล่องตรงกัน — เห็นชัดตอนตัวหนังสือคนละขนาด)
รายละเอียดที่ทำให้ดูรู้จริง
ค่าเริ่มต้นจริง ๆ ของ align-items ตามมาตรฐานคือคำว่า normal
ไม่ใช่ stretch — แต่ normal เมื่ออยู่ใน flex container ให้ผลเหมือน
stretch · คุยกันเร็ว ๆ พูดว่า "ค่าเริ่มต้นคือ stretch" ได้เลย
แต่ถ้าเปิด DevTools แล้วเห็น normal จะได้ไม่ตกใจว่าใครไปแก้
10 · pseudo-class ของ state + ข้อควรระวัง
| เขียน | เป็นจริงตอน | ใช้กับ | ต้องระวัง |
|---|---|---|---|
:hover | ตัวชี้ลอยอยู่เหนือ (ไม่เกี่ยวกับการคลิก) | element อะไรก็ได้ | มือถือไม่มี — ห้ามซ่อนของสำคัญไว้หลังมัน |
:focus | คีย์บอร์ดคุยอยู่กับตัวนี้ (คลิกก็นับ) | ตัวที่โฟกัสได้ | อย่าใส่ outline: none แล้วจบแค่นั้น |
:focus-visible | โฟกัสอยู่ และ เบราว์เซอร์เห็นว่าควรโชว์กรอบ (คือตอนใช้คีย์บอร์ด) | ตัวที่โฟกัสได้ | ต้องเขียนหลัง :focus ไม่งั้นโดนทับ |
:active | กำลังกดค้างอยู่ (designer เรียก pressed) | element อะไรก็ได้ | ต้องเขียนไว้ล่างสุด ไม่งั้น :hover ทับ |
:disabled / :enabled | มี / ไม่มี attribute disabled | form control เท่านั้น | <div> กับ <a> ใช้ไม่ได้ — กฎเงียบไปเฉย ๆ ไม่ error |
ปุ่มทั่วไป: ปกติ → :hover →
:focus-visible → :active → :disabled
ลิงก์ <a>: LVHA — :link →
:visited → :hover → :active (จำว่า "LoVe—HAte")
เหตุผลเดียวกันทั้งคู่: pseudo-class พวกนี้ specificity เท่ากันหมด ตัวล่างสุดจึงชนะเสมอ —
เรียงจาก "เกิดบ่อย/ถาวร" ไปหา "เกิดชั่วขณะ" แล้วจะไม่มีวันทับกันผิด
❌ ลบทิ้งเฉย ๆ ไม่วาดอะไรกลับมา
button:focus { outline: none; }
ลบกรอบทุกกรณี รวมทั้งตอนกด Tab · คนที่ใช้คีย์บอร์ดจะไม่รู้ว่าตอนนี้อยู่ปุ่มไหน
— ไม่ได้มีแค่ผู้พิการ รวมถึงคนที่เมาส์เสียและคนที่ไม่อยากละมือจากคีย์บอร์ดด้วย
✅ ลบของ default แล้ววาดของเราแทน
.btn:focus { outline: none; }
.btn:focus-visible { outline: 3px solid #1a73e8;
outline-offset: 3px; }
คลิกด้วยเมาส์ = ไม่มีกรอบค้าง · กด Tab มา = เห็นวงแหวนชัด ·
เขียน :focus ก่อน :focus-visible เสมอ
โทรศัพท์ไม่มีตัวชี้ที่ "ลอยอยู่เหนือ" อะไรได้ · เอาข้อมูลที่จำเป็นไปซ่อนหลัง
:hover = คนใช้มือถือไม่มีทางเห็นมันเลย ไม่ใช่ "เห็นยาก"
กฎที่ปลอดภัย: :hover ใช้ตกแต่งได้ ใช้ซ่อนของไม่ได้
อยากเขียนสไตล์ที่เฉพาะเครื่องมีเมาส์จริงถึงใช้ ให้ครอบด้วย
@media (hover: hover) { … } — ถามความสามารถของอุปกรณ์ ไม่ใช่ถามความกว้างจอ
(แท็บเล็ตจอใหญ่ก็ไม่มี hover · หน้าต่างที่ย่อแคบบน desktop ก็ยังมีเมาส์)
disabled (attribute) ≠ :disabled (pseudo-class)
attribute disabled ใน HTML ทำสามอย่างให้ทันทีโดยไม่ต้องเขียนอะไรเลย:
กดแล้ว event click ไม่ยิงออกมาเลย · กด Tab ข้ามมันไป ·
และค่าไม่ถูกส่งไปกับฟอร์ม · ส่วน CSS แค่ทำให้จาง ไม่ได้ปิดอะไรเลย — ต้องทำทั้งสองอย่าง
ทดสอบด้วย el.matches(…) | ผลจริง |
|---|---|
<input> ธรรมดา ตรงกับ :enabled ไหม | true |
<div> ตรงกับ :enabled ไหม | false |
<a href="#"> ตรงกับ :enabled ไหม | false |
<button> ที่อยู่ใน <fieldset disabled> ตรงกับ :disabled ไหม | true — ปิดยกชุดได้ |
เรียก .click() ใส่ปุ่มที่ disabled — handler ยิงกี่ครั้ง | 0 ครั้ง (ปุ่มปกติได้ 1) |
อยากได้พฤติกรรมของปุ่ม → ใช้ <button> จริง ๆ
แล้วจะได้ :disabled · การข้าม Tab · และการปิด click มาฟรีทั้งชุด
"Dragged" ในสไลด์
รายการ state ในสไลด์ (enabled · hover · focused · pressed · disabled · dragged)
คือรายการในภาษาของ design system ซึ่ง Material Design 3 ใช้คำชุดนี้เป๊ะ
ไม่ใช่รายการ pseudo-class ของ CSS · ห้าตัวแรกมีให้ใช้ตรงตัว
(:enabled · :hover · :focus · :active · :disabled)
ส่วน :dragged ไม่มีอยู่จริง — ถามเบราว์เซอร์ตรง ๆ ด้วย
CSS.supports('selector(:dragged)') ได้ false ·
ที่ทำได้ด้วย CSS ล้วนคือ cursor: grab คู่กับ :active { cursor: grabbing } ·
การลากจริงต้องใช้ JavaScript แปะ class เอง เช่น .is-dragging
11 · ตารางกับดักรวมทั้งกลุ่ม
เรียงตามอาการที่เห็นด้วยตา เพราะเวลาติดจริง คุณมีแค่อาการ ไม่ได้มีชื่อของปัญหา
| อาการที่เห็น | สาเหตุ | ทางแก้ |
|---|---|---|
ภาษาไทยกลายเป็น สวัส… ทั้งหน้า | ลืม <meta charset="UTF-8"> · เบราว์เซอร์เดาเป็น windows-1252 | ใส่บรรทัดนั้นใน <head> (บท 56) |
หน้าดูปกติทุกอย่าง แต่ document.compatMode ได้ BackCompat | ไม่มี <!DOCTYPE html> หรือมีอะไรอยู่ข้างหน้ามัน | ใส่กลับที่บรรทัดแรกสุด — บั๊กที่รอ คือบั๊กที่แพงที่สุด (บท 56) |
| แก้สีที่ไฟล์ CSS กลางแล้ว แต่บางตัวไม่ยอมเปลี่ยน | มี style="" ติดที่ตัว element ซึ่งชนะกฎกลางเสมอ | ถอด inline style ออก ย้ายไปเป็น class (บท 56) |
ใส่ padding ให้ <p> แล้วไม่ครอบของข้างใน · เจอ <p></p> เปล่าที่ไม่ได้พิมพ์ | เอา block ไปใส่ใน <p> · parser แอบปิด </p> ให้ | สลับข้าง — เอา <div> ครอบ <p> · ดู DOM จริงใน DevTools ไม่ใช่ไฟล์ต้นฉบับ (บท 57) |
สั่ง width/height แล้วไม่เกิดอะไรขึ้นเลย | element นั้นเป็น inline (span · a · strong · img) | เปิด DevTools ดู display ก่อนทำอย่างอื่น → inline-block (บท 57 · 60) |
เปลี่ยน <div id="main"> เป็น <main> แล้วหน้าตาไม่เปลี่ยนเลย | ถูกแล้ว — วัดได้ 482px · display: block เหมือนกันเป๊ะ | ประโยชน์อยู่ที่ landmark ซึ่งไม่โผล่บนหน้าจอ — อย่าตัดสินด้วย "เปลี่ยนแล้วเหมือนเดิม" (บท 57) |
| คลิกที่ข้อความของ label แล้วช่องไม่โฟกัส | for สะกดไม่ตรงกับ id · เบราว์เซอร์ไม่หาตัวที่ "ใกล้เคียง" ให้เลย | ทดสอบด้วยการคลิกที่ข้อความทุกอัน · ตรวจว่า id ไม่ซ้ำในหน้า (บท 58) |
| กดปุ่มแล้วหน้ากะพริบ แล้วทุกอย่างที่พิมพ์หายหมด | <button> ในฟอร์มที่ไม่ใส่ type = submit โดยปริยาย | type="button" ให้ทุกปุ่มที่ไม่ใช่ปุ่มส่ง — และเขียน type ให้ทุกปุ่มเสมอ (บท 58) |
| กรอกครบทุกช่อง แต่ฝั่ง Go อ่านได้ไม่ครบ | ช่องนั้นไม่มี name — ไม่มีชื่อให้เขียน ก็ถูกข้ามเงียบ ๆ | ใส่ name ให้ทุกช่องที่ต้องส่ง · ดู query string หลังกดส่งด้วยตาตัวเอง (บท 58) |
| คนแก้คำภาษาไทยใน dropdown แล้วค่าใน database เปลี่ยนตาม | <option> ไม่มี value → ส่งข้อความข้างในไปแทน | ใส่ value ให้ทุก <option> เสมอ (บท 58) |
| ฝั่ง Gin อ่าน body ของฟอร์มไม่ออก | ฟอร์ม POST ส่ง application/x-www-form-urlencoded ไม่ใช่ JSON | c.PostForm หรือ tag form:"…" — ไม่ใช่ c.ShouldBindJSON (บท 58) |
| เขียน CSS แล้วไม่เห็นอะไรเปลี่ยน · เปิด DevTools แล้วไม่เห็นกฎนั้นเลย | ลืมจุดหรือชาร์ปหน้า selector — sub-title { } กลายเป็นการหา element ชื่อ sub-title | เติม . / # · เบราว์เซอร์ไม่แจ้ง error ให้ มันเงียบไปเฉย ๆ (บท 59) |
| เห็นกฎในแผง Styles แต่ถูกขีดฆ่าคาไว้ | กฎติดแล้ว แต่แพ้ specificity | ลดน้ำหนักฝ่ายที่ชนะ (เลิกใช้ #id จัดสไตล์) อย่าเพิ่มน้ำหนักฝั่งเรา (บท 59) |
| นั่งสลับบรรทัดใน CSS ทั้งวันแล้วหน้าไม่เปลี่ยนเลย | ปัญหาอยู่ที่น้ำหนัก ไม่ใช่ลำดับ — ลำดับตัดสินก็ต่อเมื่อน้ำหนักเสมอกัน | เทียบสามช่อง (id, class, tag) จากซ้ายไปขวาก่อนเสมอ (บท 59) |
| กล่องล้นออกนอกพ่อ · scrollbar แนวนอนโผล่มาเอง | width: 100% ในโหมด content-box นับเฉพาะ content · padding + border บวกข้างนอก (300 → 348) | box-sizing: border-box — โปรเจกต์จริงตั้ง * { box-sizing: border-box; } ไว้บรรทัดแรก (บท 60) |
เพิ่มตัวเลข margin แล้วระยะห่างไม่ขยับเลย | margin แนวตั้งยุบรวมกัน แล้วใช้ค่าที่มากกว่า — max(40, 24) = 40 | อย่าเพิ่มตัวเลขมั่ว → เปลี่ยนไปใช้ gap ของ flex/grid จบเรื่องเร็วกว่า (บท 60) |
ใส่ margin-top ให้ลูก แล้วทั้งก้อนเลื่อนลงไปทั้งอัน | margin ทะลุออกนอกพ่อ เพราะพ่อไม่มี padding/border คั่น | padding-top: 1px ให้พ่อก็หยุดได้ · หรือทำพ่อเป็น flex (บท 60) |
ตัวหนังสือบานใหญ่ผิดคาดทั้งที่ CSS เขียน 1.5em เท่ากันทุกชั้น | em ทบต้น — 20 → 30 → 45px | ใช้ rem เป็นค่าเริ่มต้น เก็บ em ไว้ใช้เฉพาะที่ตั้งใจ (บท 60) |
ตั้ง width: 300px ให้การ์ด แล้วเปิดดูได้แคบกว่านั้น | อยู่ใน flex container ที่เป็น nowrap แล้วโดน flex-shrink: 1 บีบ | เลือกตามเจตนา: flex-wrap: wrap ที่แม่ · flex-shrink: 0 ที่ลูก · หรือ min-width (บท 61) |
ใส่ flex: 1 แล้วเงียบสนิท | กล่องนั้นไม่ใช่ลูกชั้นแรกของ flex container (เป็นหลานอีกชั้น) หรือแม่ยังไม่ display: flex | เช็คว่าใครเป็นแม่จริง ๆ · flex item = ลูกชั้นแรกเท่านั้น (บท 61) |
ใส่ align-items: center แล้วไม่เห็นความต่าง | แม่ไม่ได้สูงกว่าเนื้อหา จึงไม่มีที่ว่างในแนวแกนรองให้จัด | ใส่ height หรือ padding ให้แม่ก่อน (บท 61) |
ใส่ justify-content แล้วไม่ขยับ | ไม่มีที่ว่างเหลือให้กระจาย เพราะของเต็มแถวพอดีอยู่แล้ว | ดูว่าลูกกินที่หมดหรือยัง ก่อนไปสงสัยว่าเขียนผิด (บท 61) |
flex-grow: -1 เขียนไปแล้วไม่ทำงาน แถวไม่มี error ใน Console | ค่าติดลบใช้ไม่ได้ — เบราว์เซอร์ทิ้งบรรทัดนั้นทั้งบรรทัดแล้วกลับไปใช้ค่าเริ่มต้น | ที่เดียวที่เห็นคือแผง Styles ใน DevTools ซึ่งขีดฆ่าบรรทัดนั้นไว้ให้ (บท 61) |
flex: 1 กับ flex-grow: 1 ให้ผลไม่เท่ากัน | flex: 1 พ่วง flex-basis: 0% มาด้วย — 146/146 เทียบกับ 116/176 | อยากได้เท่ากัน → flex: 1 · อยากให้เนื้อหาสั้นแคบหน่อย → flex: auto (บท 61) |
| ใส่ URL ยาว ๆ ในการ์ดแล้วทั้งแถวล้นทะลุกรอบ | flex item หดได้ถึงแค่ min-content (ความกว้างของคำที่ตัดบรรทัดไม่ได้) แล้วหยุด | min-width: 0 ถึงจะยอมหดต่อ (บท 61) |
กดปุ่มค้างด้วยเมาส์แล้วสีของ :active ไม่โผล่ | ตอนกดค้าง :hover ก็จริงด้วย · specificity เท่ากัน → ตัวที่เขียนทีหลังชนะ | ย้าย :active ลงล่างสุด — แก้ด้วยการย้ายบรรทัด ไม่ใช่เพิ่ม !important (บท 62) |
กด Tab แล้วไม่รู้ว่าตอนนี้อยู่ปุ่มไหนของหน้า | มีคนเขียน button:focus { outline: none; } ทิ้งไว้โดยไม่วาดอะไรกลับมา | ย้ายกรอบไป :focus-visible · เบราว์เซอร์ไม่ใส่กลับมาให้เอง (บท 62) |
เขียน .my-button:disabled ให้ <div> แล้วไม่มีอะไรเกิดขึ้น | :disabled/:enabled ใช้กับ form control เท่านั้น — <div> ได้ false | ใช้ <button> จริง ๆ · selector ที่ไม่ตรงกับอะไรถูกข้ามเงียบ ๆ ไม่เคยทำให้ไฟล์พัง (บท 62) |
| ปุ่มจางลงแล้ว แต่ผู้ใช้ยังกดได้อยู่ | CSS ทำให้จางอย่างเดียว ไม่ได้ปิดอะไรเลย | แปะ attribute disabled ใน HTML ด้วยเสมอ (บท 62) |
| คนใช้มือถือบอกว่าไม่เห็นราคา/ปุ่มที่คุณเห็นอยู่ชัด ๆ | ของนั้นถูกซ่อนไว้หลัง :hover ซึ่งมือถือไม่มี | โชว์ตลอด แล้วใช้ :hover แค่เน้นของที่เห็นอยู่แล้ว (บท 62) |
12 · ที่ฝึกต่อ และคู่มือที่ควรรู้ว่ามีอยู่
สองชุดนี้คนละหน้าที่กันเลย — ชุดแรกไว้ฝึกมือ ชุดที่สองไว้เปิดตอนต้องตัดสินใจ
| เกมฝึกมือ | ฝึกอะไร | ควรเล่นตอนไหน |
|---|---|---|
| CSS Diner | selector ล้วน ๆ 32 ด่าน ตั้งแต่ div จนถึง selector ซับซ้อน | เล่นก่อนเพื่อนเลย — selector คือของที่ใช้ทุกวันและฝึกได้เร็วที่สุด |
| Flexbox Froggy | flexbox 24 ด่าน — พากบไปเกาะใบบัวด้วย justify-content / align-items | หลังบท 61 · ด่านละไม่ถึงนาที จบได้ในนั่งเดียว |
| Flexbox Zombies | flexbox เหมือนกัน แต่ยาวกว่าและลึกกว่า มี 12 บทแยกตาม property | ถ้าเล่น Froggy จบแล้วยังไม่มั่นใจ — ตัวนี้บังคับให้ใช้ property เดียวจนคล่องทีละบท |
| Coding Fantasy | เกม RPG ที่ใช้ CSS (และภาษาอื่น) เป็นอาวุธ | เล่นเอาสนุก · ไม่ใช่ทางลัดของเรื่องไหนโดยเฉพาะ |
| คู่มือ | เอาไว้ตอบคำถามแบบไหน | เข้าไปแล้วดูตรงไหนก่อน |
|---|---|---|
| CSS-Tricks Guides | "property นี้มันทำงานยังไงกันแน่" | หน้า A Complete Guide to Flexbox — เป็นหน้าที่คนทำเว็บทั้งโลกเปิดบ่อยที่สุดหน้าหนึ่ง · ใช้เป็นแผ่นภาพ ไม่ใช่บทเรียน |
| Material Design 3 | "ปุ่มตอน hover ควรเข้มขึ้นเท่าไหร่ ตอน pressed ควรเป็นยังไง" | Foundations → Interaction states — ที่มาโดยตรงของรายการ state ในสไลด์ |
| Apple HIG | "ปุ่มควรใหญ่แค่ไหนถึงกดโดนบนมือถือ" | หัวข้อ Inputs และ Foundations → Accessibility |
| WCAG 2.1 | "เว็บนี้คนที่มองไม่เห็น / ใช้เมาส์ไม่ได้ ใช้ได้ไหม" | ข้อ 2.4.7 Focus Visible ข้อเดียวก่อน — สั้นมาก และมันคือหัวข้อ 10 ของหน้านี้ทั้งดุ้น |
💬 ถามได้ เจออาการที่ไม่มีในตารางหัวข้อ 11 ส่งมาได้เลย — บอกมาว่าคาดว่าจะได้อะไร แต่ได้อะไรจริง สองอย่างนี้พอสำหรับหาสาเหตุแล้ว · ถ้าถาม AI ให้แปะโค้ดพร้อมประโยคนี้: "this is plain HTML/CSS, no framework — tell me what the browser does with it line by line"
อ้างอิง
- MDN — How the web works — อ่านหัวข้อ So what happens, exactly? · เทียบกับหัวข้อ 1 ของหน้านี้
- MDN —
<meta>— อ่านแค่หัวข้อcharsetกับviewport· Document.compatMode อ่านตารางค่าที่คืนได้อย่างเดียวก็พอ - MDN — Block and inline layout in normal flow — สองย่อหน้าแรกครบเนื้อหาของหัวข้อ 2 · MDN —
<main>อ่าน Usage notes - MDN —
<p>— อ่าน Technical summary ตรงบรรทัด Permitted content · ต้นฉบับของกฎอยู่ที่ WHATWG — the p element - W3C WAI — An alt Decision Tree — แผนผังตอบคำถาม "รูปนี้ควรใส่ alt ว่าอะไร" ในหน้าเดียว
- MDN —
<input>types · MDN —<label>(หัวข้อ Accessibility) · MDN —<button>attribute type (หลักฐานว่าค่าเริ่มต้นคือsubmit) - MDN — Sending form data · gin.Context.PostForm — บรรทัดเดียวใต้ชื่อฟังก์ชันบอกว่ามันอ่าน urlencoded form
- MDN — CSS selectors (อ่าน Basic selectors) · MDN — Specificity (ตาราง Selector weight categories คือที่มาของสามช่อง) · W3C Selectors Level 4 §17
- MDN — Cascade (หัวข้อ Order of precedence) · MDN — the id attribute (สองย่อหน้าแรก)
- MDN — The box model · box-sizing · Mastering margin collapsing (สามกรณีที่การยุบรวมเกิดขึ้น)
- MDN — CSS values and units (ตาราง Relative length units) · MDN — display · linear-gradient()
- MDN — Basic concepts of flexbox (อ่าน The two axes of flexbox ก่อนหัวข้ออื่น) · MDN — flex · MDN — align-items (บรรทัด Initial value เขียนว่า
normal) · W3C — CSS Flexible Box §7.1.1 - MDN — :focus-visible (หัวข้อ focus-visible vs focus) · MDN — :disabled (รายการ element ที่ใช้ได้ — มีแต่ form control กับ
<fieldset>) · MDN — media feature hover