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

กล่องกับระยะห่าง — padding, margin, border และหน่วยที่ใช้

~20 นาที · ควรผ่าน บทเรียน 59 (เขียน selector เป็น) มาก่อน · ไม่ต้องรู้อะไรเพิ่มนอกจากนั้น

บทนี้มาจากสไลด์ หน้า 37–47 (CSS Basic Styles) ซึ่งไล่เรียง property ทีละตัว — font-size · color · padding · margin · border · หน่วย · display
แต่ผมจะไม่สอนเรียงแบบนั้น เพราะ property เหล่านี้ไม่ได้เป็นอิสระต่อกัน ทั้งหมดวางอยู่บนโมเดลเดียวกันชื่อ box model ถ้าเข้าใจโมเดลก่อน property จะเหลือแค่ชื่อที่ต้องจำ
และในนั้นมีกับดักสองอันที่มือใหม่ทุกคนเจอ — กล่องที่ล้นออกนอกกรอบทั้งที่สั่ง width: 100% กับระยะห่างที่หายไปเฉย ๆ ทั้งที่ใส่ตัวเลขไว้แล้ว
บทนี้ไม่บรรยายว่า "จะเห็นเป็น…" — ทุกกล่องผลลัพธ์ในหน้านี้เรนเดอร์จริงในเบราว์เซอร์ของคุณ และตัวเลขทุกตัวผมวัดมาจาก Chrome จริงก่อนเขียน

🧰 ของที่ใช้ตรวจในบทนี้ (รันวันที่ 27 สิงหาคม 2026)

Google Chrome (headless) บน macOS
// วัดด้วย getBoundingClientRect() และ getComputedStyle()

ตัวเลขทุกตัวในบทนี้ (348px · 40px · 45px · 20px …) มาจากการวัดจริง ไม่ได้คำนวณในหัว
กล่อง ผลลัพธ์จริง ที่เห็นในหน้านี้คือ <iframe> ที่รันโค้ดชุดเดียวกับกล่องโค้ดข้างบนมันเป๊ะ ๆ ไม่มี CSS ของหน้าเว็บนี้เจือปน

1 · ทุกอย่างบนหน้าเว็บคือกล่องสี่เหลี่ยม

ประโยคเดียวที่ต้องเชื่อก่อนเริ่ม: ไม่ว่าคุณจะเขียน element อะไร เบราว์เซอร์มองมันเป็นสี่เหลี่ยมเสมอ
ตัวอักษรกลม ๆ ปุ่มขอบมน รูปวงกลม — ข้างในเบราว์เซอร์มันคือสี่เหลี่ยมทั้งหมด แค่ทาสีให้ดูไม่เหมือน

และสี่เหลี่ยมหนึ่งอันไม่ได้มีชั้นเดียว มันมีสี่ชั้นซ้อนกัน จากในออกนอก:

   +-----------------------------------------+
   |  MARGIN                                 |
   |   +-----------------------------------+ |
   |   |  BORDER                           | |
   |   |   +-----------------------------+ | |
   |   |   |  PADDING                    | | |
   |   |   |   +---------------------+   | | |
   |   |   |   |  CONTENT            |   | | |
   |   |   |   +---------------------+   | | |
   |   |   |                             | | |
   |   |   +-----------------------------+ | |
   |   |                                   | |
   |   +-----------------------------------+ |
   |                                         |
   +-----------------------------------------+
ชั้นคืออะไรพื้นหลังทาถึงไหม
contentตัวข้อความ รูป หรือกล่องลูกที่อยู่ข้างในทา
paddingที่ว่างด้านใน ระหว่างเนื้อหากับขอบกล่องทา
borderเส้นกรอบ — มีความหนาจริง กินที่จริงเป็นเส้นเอง
marginที่ว่างด้านนอก ระหว่างกล่องนี้กับกล่องอื่นไม่ทา — โปร่งใสเสมอ

ช่องขวาสุดของตารางนี้คือวิธีดูด้วยตาเปล่าว่าอันไหนคือ padding อันไหนคือ margin — ทาสีพื้นให้กล่อง แล้วดูว่าสีขยายตามไปด้วยหรือไม่ ถ้าขยาย = padding ถ้าไม่ขยาย = margin

<style>
  .wrap { background: #eef2f7; border: 2px dashed #74c0fc; }
  .pad  { background: #ffd8a8; padding: 24px; }
  .mar  { background: #a5d8ff; margin: 24px; }
</style>

<div class="wrap">
  <div class="pad">padding: 24px — พื้นสีส้มขยายออกไปด้วย</div>
  <div class="mar">margin: 24px — พื้นสีฟ้าไม่ขยาย ที่ว่างเป็นสีของพ่อ</div>
</div>

สังเกตให้ดี: กล่องส้มกับกล่องฟ้าเว้นที่ไว้เท่ากันคือ 24px แต่หน้าตาต่างกันคนละเรื่อง — ส้มกลืนที่ว่างเข้ามาเป็นของตัวเอง ส่วนฟ้ายกที่ว่างนั้นให้พ่อ (พื้นเทา) ไป

📦 เทียบให้เห็นภาพ — กล่องพัสดุบนชั้นวางของ

นึกถึงกล่องพัสดุที่วางเรียงกันบนชั้น:
· content = ของที่อยู่ข้างใน เช่น แก้วน้ำ
· padding = โฟมกันกระแทกที่ยัดไว้รอบแก้ว — อยู่ในกล่อง ยัดเยอะกล่องก็ใหญ่ขึ้น
· border = ผนังกระดาษของกล่องเอง — บางหรือหนาก็ได้ แต่มีความหนาจริง
· margin = ช่องว่างที่คุณเว้นไว้รอบนอกกล่อง ไม่ให้มันชนกล่องข้าง ๆ

เอาไปใช้ตัดสินใจได้เลย: อยากให้ข้อความห่างจากขอบกล่องตัวเอง → padding · อยากให้กล่องห่างจากกล่องอื่น → margin

ที่ไม่เหมือนกัน: ช่องว่างบนชั้นวางของจริง ถ้าคุณเว้น 5 ซม. และคนข้าง ๆ เว้น 3 ซม. ระยะห่างจริงคือ 8 ซม. — แต่ margin ในแนวตั้งไม่บวกกันแบบนั้น มันเลือกเอาค่ามากกว่าค่าเดียว (จะได้เห็นของจริงในหัวข้อ 5)
อีกจุด: โฟมกันกระแทกยัดเข้าไปแล้วกล่องข้างนอกไม่โตขึ้นก็ได้ ถ้าคุณยัดของน้อยลง — แต่ padding ในค่าเริ่มต้นของ CSS ดันกล่องให้ใหญ่ขึ้นเสมอ (หัวข้อ 4)

🗣 ศัพท์ที่เพิ่งเรียนไป
box model
โมเดลที่บอกว่ากล่องหนึ่งกล่องประกอบด้วยสี่ชั้น content · padding · border · margin และแต่ละชั้นกินที่เท่าไหร่ · ถาม senior ได้ว่า “อันนี้ควรแก้ที่ padding หรือ margin” เป็นคำถามที่คนทำเว็บทุกคนเข้าใจทันที ต่างจากการถามว่า “ทำไมมันชิดกันจัง”
padding
ที่ว่างด้านในกล่อง ระหว่างเนื้อหากับขอบ · พื้นหลังทาถึง · พิมพ์บอก AI ได้ว่า “give the card 24px of inner padding, not margin” การระบุคำว่า inner ทำให้ไม่ต้องเถียงกันรอบสอง
margin
ที่ว่างด้านนอกกล่อง ระหว่างกล่องนี้กับกล่องอื่น · โปร่งใสเสมอ · พิมพ์บอก AI ได้ว่า “add 16px of vertical spacing between these cards” แล้วค่อยดูว่ามันเลือกใช้ margin หรือ gap
border
เส้นกรอบของกล่อง มีทั้งความหนา (width) รูปแบบ (style) และสี (color) · จำไว้ว่ามันกินที่จริง ไม่ใช่เส้นวาดทับ — จุดนี้จะกลับมาหลอกคุณในหัวข้อ 4

2 · เขียน padding กับ margin — สี่แบบที่เจอในโค้ดจริง

สไลด์หน้า 43 กับ 44 โชว์รูปแบบการเขียนไว้ครบแล้ว ทั้งสอง property ใช้กติกาเดียวกันเป๊ะ เปลี่ยนแค่ชื่อ ดังนั้นจำครั้งเดียวได้ทั้งคู่:

เขียนแบบตัวอย่างแปลว่า
1 ค่าpadding: 64px;ทั้งสี่ด้าน 64
2 ค่าpadding: 120px 64px;บน/ล่าง 120 · ซ้าย/ขวา 64
4 ค่าpadding: 120px 64px 120px 64px;บน · ขวา · ล่าง · ซ้าย (วนตามเข็มนาฬิกา)
แยกด้านpadding-top: 120px;ระบุทีละด้าน มีครบสี่ตัว -top -right -bottom -left

เคล็ดจำลำดับ 4 ค่า: เริ่มที่บน แล้วเดินตามเข็มนาฬิกา — บน → ขวา → ล่าง → ซ้าย
ส่วนแบบ 2 ค่าคือ "แนวตั้งก่อน แนวนอนทีหลัง" ซึ่งเป็นแบบที่เจอบ่อยที่สุดในโค้ดจริง เพราะการ์ดส่วนใหญ่อยากให้บนล่างกับซ้ายขวาไม่เท่ากัน

margin มีของแถมที่ padding ไม่มีอยู่สองอย่าง: รับค่าติดลบได้ (ดึงกล่องให้ทับกัน) และรับคำว่า auto ได้ ซึ่ง margin: 0 auto; คือวิธีจัดกล่องให้อยู่กลางแนวนอนที่คุณจะเจอในทุกโปรเจกต์

3 · border — สามอย่างในบรรทัดเดียว

border เป็น shorthand ที่รวมสามค่าไว้ในบรรทัดเดียว: ความหนา · รูปแบบ · สี
ที่ต้องรู้คือ ถ้าไม่ใส่ style เส้นจะไม่ขึ้นเลย — ค่าเริ่มต้นของ border-style คือ none ใส่ border: 4px red; อย่างเดียวจึงไม่เห็นอะไรทั้งนั้น

<style>
  .b1 { border: 4px solid #495057;  padding: 8px; margin-bottom: 12px; }
  .b2 { border: 4px dashed #e8590c; padding: 8px; margin-bottom: 12px; }
  .b3 { border-width: 4px;
        border-style: solid;
        border-color: #1971c2;
        border-radius: 12px;
        padding: 8px; }
</style>

<div class="b1">border: 4px solid #495057;</div>
<div class="b2">border: 4px dashed #e8590c;</div>
<div class="b3">เขียนแยกสามบรรทัด + border-radius</div>

สองบรรทัดแรกกับสามบรรทัดหลังให้ผลเหมือนกัน ต่างกันแค่ความยาว — ในโค้ดจริงคนใช้ shorthand เกือบตลอด แล้วค่อยใช้แบบแยกเมื่อต้องแก้เฉพาะบางส่วนทีหลัง เช่น border-color: red; ตอน hover

4 · กับดักที่หนึ่ง — width: 100% แล้วล้น

นี่คือกับดักที่มือใหม่เจอเป็นอันดับหนึ่ง และเจอภายในชั่วโมงแรกที่เริ่มจัดหน้า
ลองทายก่อนดูเฉลย:

.frame { width: 300px; }

.card  { width: 100%;
          padding: 20px;
          border: 4px solid red; }

/* .card เป็นลูกของ .frame */

กล่อง .card จะกว้างจริง ๆ กี่ pixel?

348px — ไม่ใช่ 300 (ผมวัดด้วย getBoundingClientRect() ใน Chrome ได้ 348 เป๊ะ)

เพราะ width: 100% ในค่าเริ่มต้นของ CSS หมายถึง ความกว้างของ content เท่านั้น ส่วน padding กับ border ไปบวกเพิ่มข้างนอก:
300 (content) + 20×2 (padding) + 4×2 (border) = 348
ผลคือกล่องลูกกว้างกว่ากล่องพ่อ 48px แล้วล้นออกไป

<style>
  .frame { width: 300px; border: 2px dashed #adb5bd; }
  .card  { width: 100%;
           padding: 20px;
           border: 4px solid #c92a2a;
           background: #ffe3e3; }
</style>

<div class="frame">
  <div class="card">width: 100% + padding 20 + border 4 → ล้นออกนอกกรอบ</div>
</div>

เส้นประเทาคือกรอบ 300px · กล่องแดงคือลูกที่สั่ง width: 100% — จะเห็นชัดว่ามันโผล่พ้นเส้นประออกไปทั้งสองข้าง

ทางแก้คือบรรทัดเดียว: box-sizing: border-box; ซึ่งเปลี่ยนความหมายของ width จาก "ความกว้างของ content" เป็น "ความกว้างรวม padding และ border แล้ว"

<style>
  .frame { width: 300px; border: 2px dashed #adb5bd; }
  .card  { width: 100%;
           padding: 20px;
           border: 4px solid #2f9e44;
           background: #d3f9d8;
           box-sizing: border-box; }
</style>

<div class="frame">
  <div class="card">เพิ่ม box-sizing: border-box บรรทัดเดียว → พอดี 300px</div>
</div>

คราวนี้กล่องเขียวพอดีกับเส้นประเป๊ะ · ผมวัดได้ 300 ตรง ๆ และที่ว่างข้างในหดจาก 300 เหลือ 300 − 20×2 − 4×2 = 252 — นั่นคือสิ่งที่ border-box ทำ: มันไม่ได้ทำให้ padding หายไป มันเบียดที่ของ content แทน

🪟 เทียบให้เห็นภาพ — สั่งกระจก กับ สั่งกรอบหน้าต่าง

คุณโทรสั่งช่างว่า "เอากว้าง 300" ช่างมีสองวิธีตีความ:
· วัดเฉพาะแผ่นกระจก — กระจก 300 พอดี แต่พอใส่กรอบเข้าไป ของที่ต้องยัดเข้าช่องหน้าต่างจริง ๆ กลายเป็น 348 ใส่ไม่ลง
· วัดรวมกรอบ — ของทั้งชิ้น 300 พอดี ใส่ลงช่องแน่นอน แค่กระจกข้างในเล็กลงหน่อย

ค่าเริ่มต้นของ CSS คือวิธีแรก (content-box) · border-box คือวิธีที่สอง
และนี่คือเหตุผลที่โปรเจกต์จริงเกือบทุกโปรเจกต์เขียน * { box-sizing: border-box; } ไว้บรรทัดแรกของไฟล์ CSS แล้วไม่ต้องคิดเรื่องนี้อีกเลย

ที่ไม่เหมือนกัน: ช่างจริงจะโทรกลับมาถามว่า "พี่หมายถึงรวมกรอบหรือเปล่าครับ" — แต่ CSS ไม่ถาม มันเลือกวิธีแรกให้เงียบ ๆ แล้วปล่อยของล้นออกมา
และไม่มี error ไม่มี warning ใน console ด้วย — คุณรู้ตัวตอนเห็น scrollbar แนวนอนโผล่มาเท่านั้น

🗣 ศัพท์ที่เพิ่งเรียนไป
box-sizing
property ที่บอกว่า width/height ที่คุณเขียน หมายถึงวัดถึงชั้นไหน · ถาม senior ได้ว่า “โปรเจกต์นี้ตั้ง box-sizing: border-box ไว้ทั่วทั้งไฟล์หรือยัง” เป็นคำถามที่ตอบได้ใน 5 วินาที และประหยัดเวลาคุณทั้งวัน
content-box
ค่าเริ่มต้น — width นับเฉพาะ content · padding กับ border บวกเพิ่มข้างนอก · คือคำอธิบายว่าทำไม 100% ถึงล้น เวลาอธิบายให้คนอื่นฟังให้ใช้คำนี้
border-box
width นับรวม padding และ border แล้ว · content จะหดเอง · พิมพ์บอก AI ได้ว่า “apply box-sizing: border-box globally with the universal selector”
overflow (ล้น)
อาการที่เนื้อในกว้าง/สูงเกินกล่องที่ครอบมันอยู่ · ถ้าเจอ scrollbar แนวนอนโผล่มาโดยไม่ได้ตั้งใจ ให้สงสัย box-sizing เป็นอันดับแรก

5 · กับดักที่สอง — margin สองอันบนล่างกัน "ยุบรวมกัน"

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

.top { margin-bottom: 40px; }
.bot { margin-top: 24px; }

/* .top กับ .bot เป็น div ธรรมดา วางบนล่างติดกัน */

ช่องว่างระหว่างสองกล่องนี้กว้างกี่ pixel?

40px — ไม่ใช่ 64 (ผมวัดระยะจากขอบล่างของกล่องบนถึงขอบบนของกล่องล่างใน Chrome ได้ 40)

margin ในแนวตั้งของสองกล่องที่ติดกันไม่บวกกัน — มันยุบรวมเป็นก้อนเดียว แล้วเอาค่าที่มากกว่า
ในที่นี้คือ max(40, 24) = 40 · ตัวเลข 24 ที่คุณพิมพ์ไว้ไม่มีผลใด ๆ เลย
คนส่วนใหญ่ไม่รู้เรื่องนี้ แล้วไปแก้ 24 เป็น 30 เป็น 36 — ก็ยังไม่เห็นอะไรขยับ เพราะทุกค่าที่น้อยกว่า 40 ให้ผลเหมือนกันหมด

<style>
  .stack { background: #ffec99; border: 1px solid #f59f00; }
  .box   { background: #d0ebff; border: 1px solid #74c0fc; }
  .top   { margin-bottom: 40px; }
  .bot   { margin-top: 24px; }
</style>

<div class="stack">
  <div class="box top">กล่องบน — margin-bottom: 40px</div>
  <div class="box bot">กล่องล่าง — margin-top: 24px</div>
</div>

แถบเหลืองตรงกลางคือช่องว่างที่ได้จริง 40px
ทีนี้ดูกล่องถัดไป — โค้ดเหมือนเดิมทุกตัวอักษร เพิ่มแค่สองบรรทัดที่ .stack:

<style>
  .stack { background: #ffec99; border: 1px solid #f59f00;
           display: flex; flex-direction: column; }
  .box   { background: #d0ebff; border: 1px solid #74c0fc; }
  .top   { margin-bottom: 40px; }
  .bot   { margin-top: 24px; }
</style>

<div class="stack">
  <div class="box top">กล่องบน — margin-bottom: 40px</div>
  <div class="box bot">กล่องล่าง — margin-top: 24px</div>
</div>

แถบเหลืองกว้างขึ้นชัดเจน — ผมวัดได้ 64px คือ 40 + 24 บวกกันจริง ๆ
เพราะ ข้างในกล่อง flex ไม่มีการยุบรวม margin · นี่คือหนึ่งในเหตุผลที่คนสมัยนี้ เลิกใช้ margin จัดระยะระหว่างกล่อง แล้วหันไปใช้ gap ของ flex/grid แทน (เรื่องของบทเรียน 61)

⚠️ margin ยัง "ทะลุ" ออกจากกล่องพ่อได้ด้วย

การยุบรวมไม่ได้เกิดแค่ระหว่างพี่น้อง มันเกิดระหว่างพ่อกับลูกคนแรกด้วย — ถ้าพ่อไม่มี padding และไม่มี border คั่นไว้ margin-top ของลูกจะเด้งออกไปอยู่นอกพ่อ แทนที่จะดันลูกลงมาข้างใน

ผมวัดของจริงมาแล้ว: พ่อ div เปล่า ๆ ที่มีลูกหนึ่งตัวสั่ง margin-top: 30px → ขอบบนของพ่ออยู่ตำแหน่งเดียวกับขอบบนของลูกเป๊ะ และพ่อสูงแค่ 18px (สูงเท่าบรรทัดข้อความ) คือ 30px นั้นไม่ได้อยู่ในตัวพ่อเลย
พอเติม padding-top: 1px ให้พ่อ ระยะจากขอบพ่อถึงขอบลูกกลายเป็น 31px ทันที (1 + 30) — 1 pixel เดียวหยุดการยุบได้
เจออาการ "ใส่ margin-top ให้ลูกแล้วทั้งก้อนเลื่อนลงไปทั้งอัน" เมื่อไหร่ ให้นึกถึงข้อนี้

🗣 ศัพท์ที่เพิ่งเรียนไป
margin collapsing (การยุบรวมของ margin)
margin แนวตั้งที่มาชนกันจะรวมเป็นก้อนเดียว ใช้ค่าที่มากที่สุด ไม่บวกกัน · เกิดเฉพาะแนวตั้ง ไม่เกิดแนวนอน · ถาม senior ได้ว่า “ตรงนี้ margin มัน collapse หรือเปล่า” แทนที่จะบอกว่า “ระยะห่างมันแปลก ๆ”
gap
property ที่ใช้กับ flex/grid สำหรับเว้นระยะระหว่างลูก · ไม่ยุบรวม ไม่ทะลุออกนอกพ่อ · พิมพ์บอก AI ได้ว่า “use flex gap instead of margins for the spacing between these cards” แล้วปัญหาทั้งหัวข้อนี้หายไปพร้อมกันหมด
flex container
กล่องพ่อที่สั่ง display: flex · ลูกของมันเปลี่ยนกติกาการวางตัวใหม่หมด รวมทั้งเลิกยุบ margin · รายละเอียดเต็ม ๆ อยู่บทถัดไป

6 · หน่วย — px % rem em vh vw

สไลด์หน้า 46 ลิสต์หน่วยไว้หกตัว แต่ไม่ได้บอกว่าเลือกอันไหนเมื่อไหร่ ซึ่งคือส่วนที่ทำให้พลาดจริง มาไล่ทีละตัวว่า "ค่านี้อ้างอิงจากอะไร":

หน่วยอ้างอิงจากใช้ตอนไหน
pxไม่อ้างอิงอะไร — เป็นค่าคงที่ความหนาเส้น · ระยะเล็ก ๆ ที่ไม่ควรขยับ
%ขนาดของกล่องพ่อความกว้างที่ต้องยืดตามพ่อ
remfont-size ของ <html> (ปกติ 16px)ค่าเริ่มต้นที่ควรใช้ สำหรับ font-size และระยะห่าง
emfont-size ของตัวมันเอง — ยกเว้นตอนใช้กับ font-size ที่จะอ้างจากพ่อระยะที่ต้องโตตามตัวอักษร เช่น padding ของปุ่ม
vw1% ของความกว้างหน้าจอของที่ต้องเต็มจอ เช่น hero banner
vh1% ของความสูงหน้าจอsection สูงเต็มจอ (100vh)
⚠️ สไลด์หน้า 46 เขียน em ไว้ไม่ครบ

สไลด์เขียนว่า "Relative to the Parent's Font Size (em)" — จริงแค่ครึ่งเดียว
คำนี้ถูกเฉพาะตอนใช้ em กับ property font-size เท่านั้น
ถ้าใช้กับอย่างอื่น เช่น padding: 1em มันอ้างจาก font-size ของตัวมันเอง ไม่ใช่ของพ่อ
ผมวัดของจริง: ลูกที่ font-size: 32px อยู่ในพ่อที่ font-size: 20px แล้วสั่ง padding: 1em → ได้ 32px ไม่ใช่ 20px
จำแบบนี้ง่ายกว่า: em = "เท่ากับตัวหนังสือของกล่องนี้" ส่วนที่ font-size ต้องอ้างจากพ่อ ก็เพราะตอนนั้นตัวมันเองยังไม่มีค่าให้อ้าง

ความต่างระหว่าง em กับ rem เห็นชัดที่สุดตอนซ้อนกันหลายชั้น:

<style>
  .card { font-size: 20px; }
  .em1  { font-size: 1.5em; }
  .em2  { font-size: 1.5em; }
  .rem1 { font-size: 1.5rem; }
</style>

<div class="card">พ่อตั้ง font-size: 20px
  <div class="em1">1.5em ของฉัน = 30px
    <div class="em2">1.5em ซ้อนอีกชั้น = 45px</div>
  </div>
  <div class="rem1">1.5rem = 24px เสมอ (16 × 1.5)</div>
</div>

ตัวเลขในกล่องนั้นไม่ใช่ที่ผมเขียนเอา — ผมอ่านจาก getComputedStyle() จริง: 30px · 45px · 24px
em ทบต้น — ชั้นที่สองคือ 1.5 ของ 30 ไม่ใช่ 1.5 ของ 20 ซ้อนสี่ชั้นเมื่อไหร่ก็คำนวณในหัวไม่ทันแล้ว
ส่วน rem อ้างจากรากเดียวเสมอ ซ้อนกี่ชั้นก็ยัง 24px

📏 เทียบให้เห็นภาพ — วัดจากหลักกิโล กับ วัดจากคนที่ยืนข้างหน้า

คนต่อคิวยาว ๆ มีสองวิธีบอกว่าตัวเองอยู่ตรงไหน:
· rem = "ผมอยู่ห่างจากประตูร้าน 24 เมตร" — อ้างจากจุดตั้งต้นจุดเดียว ที่ทุกคนใช้ร่วมกัน ใครขยับก็ไม่กระทบ
· em = "ผมอยู่ห่างจากคนข้างหน้า 1.5 เท่าของช่วงแขนเขา" — ถ้าคนข้างหน้าขยับ คุณขยับตาม และคนข้างหลังคุณก็ขยับต่อ ต่อกันเป็นทอด ๆ

เอาไปใช้ได้เลย: ระยะห่างและขนาดตัวอักษรทั่วไปให้ใช้ rem · เก็บ em ไว้ใช้กับของที่อยากให้โตตามตัวหนังสือของมันเอง เช่น padding ของปุ่ม — ปุ่มตัวใหญ่ก็อยากได้ padding ใหญ่ตาม โดยไม่ต้องเขียนเพิ่ม

ที่ไม่เหมือนกัน: คนต่อคิวรู้ตัวว่าขยับตามคนข้างหน้า — แต่ em ที่ซ้อนกันสามชั้นไม่มีอะไรบอกคุณเลย คุณเห็นในไฟล์ CSS แค่ 1.5em เท่ากันทุกชั้น ต้องเปิด DevTools ดู computed value เท่านั้นถึงจะรู้ว่าจริง ๆ มันบานเป็น 45px แล้ว
อีกจุด: หลักกิโลอยู่นิ่งตลอด แต่รากของ rem ขยับได้ — ถ้าผู้ใช้ตั้งขนาดตัวอักษรในเบราว์เซอร์ให้ใหญ่ขึ้น 16 จะกลายเป็น 20 แล้วทั้งเว็บโตตาม ซึ่งเป็นข้อดี ไม่ใช่ข้อเสีย — คนสายตาไม่ดีถึงจะอ่านเว็บคุณได้

ส่วน % มีกับดักของตัวเองอยู่หนึ่งอัน ที่แทบไม่มีใครเตือน:

<style>
  .frame { width: 200px; height: 300px; border: 2px dashed #adb5bd; }
  .p     { padding: 10%; background: #e5dbff; }
</style>

<div class="frame">
  <div class="p">padding: 10% ได้ 20px ทุกด้าน — บนล่างก็คิดจากความกว้าง 200px</div>
</div>

พ่อกว้าง 200 สูง 300 · ถ้า padding-top: 10% คิดจากความสูง มันควรได้ 30px แต่ผมวัด getComputedStyle() ได้ 20px ทั้งบนและซ้าย — เพราะ % ของ padding กับ margin คิดจากความกว้างของพ่อเสมอ ทุกด้าน
ฟังดูไม่มีเหตุผล แต่มันคือกติกาที่ทำให้เทคนิคจัดสัดส่วนภาพ 16:9 ทำงานได้ — และเป็นเหตุผลที่คนไม่ค่อยใช้ % กับระยะห่าง

7 · font-size · font-family · color · background

สี่ตัวนี้ (สไลด์หน้า 38–42) ตรงไปตรงมาที่สุดในบท ไม่มีกับดักซ่อน มีแค่สองอย่างที่ควรรู้:

<style>
  .hero { font-family: "Noto Sans Thai", sans-serif;
          font-size: 20px;
          color: #ffffff;
          background-color: #364fc7;
          background-image: linear-gradient(135deg, #364fc7, #c2255c);
          padding: 24px;
          border-radius: 12px; }
  .hero .small { font-size: 0.75rem; color: #dbe4ff; }
</style>

<div class="hero">ข้อความนี้ใช้ font-size · color · background-image พร้อมกัน
  <div class="small">บรรทัดนี้ 0.75rem = 12px และสีจางลง</div>
</div>

ทำไมตัวอย่างนี้ใช้ linear-gradient() แทนรูป — สไลด์หน้า 42 เขียน background-image: url("images/example-image.png"); ซึ่งถูกต้องทุกอย่าง แต่เว็บบทเรียนนี้ตั้งใจให้เปิดได้แม้ไม่มีเน็ต จึงไม่มีรูปจากภายนอกเลยสักรูป
linear-gradient() นับเป็น "รูป" ในสายตา CSS เต็มตัว — ใช้กับ background-image ได้ตรง ๆ และไม่ต้องโหลดไฟล์อะไรเลย เวลาทำตัวอย่างหรือ mockup จึงสะดวกกว่ามาก

⚠️ รูปในสไลด์หน้า 41 ไม่ตรงกับโค้ดในสไลด์หน้า 41

สไลด์หน้า 41 เขียน background-color ไว้สองบรรทัดในกฎเดียวกัน: lightslategray แล้วตามด้วย #ffff00
แต่ภาพตัวอย่างข้าง ๆ เป็นสีเทา — ซึ่งเป็นสีของบรรทัดบน

ตามกติกาการทับซ้อน (cascade) ที่คุณเรียนไปในบทเรียน 59 เมื่อ property เดียวกันเขียนซ้ำในกฎเดียวกันและน้ำหนักเท่ากัน บรรทัดล่างชนะ ผลจริงจึงต้องเป็นสีเหลือง
ผมอ่าน getComputedStyle() มาแล้ว ได้ rgb(255, 255, 0) — เหลือง
สไลด์คงตั้งใจให้สองบรรทัดนั้นเป็น "ตัวเลือกสองแบบ" มากกว่าจะให้ใส่พร้อมกัน แต่เขียนติดกันแบบนั้นแล้วมันไม่ใช่ตัวเลือก มันคือการทับ

<style>
  .item { background-color: lightslategray;
          background-color: #ffff00;
          padding: 16px; }
</style>

<div class="item">สองบรรทัดในกฎเดียวกัน — บรรทัดล่างชนะ</div>

8 · display เบื้องต้น — กล่องนี้เป็นกล่องแบบไหน

ก่อนจะไปเรื่อง flexbox ในบทหน้า ต้องรู้ก่อนว่า display ตอบคำถามว่า "กล่องนี้วางตัวยังไงเทียบกับกล่องรอบ ๆ" — สี่ค่าที่ต้องรู้ตอนนี้:

ค่าวางตัวยังไงwidth/height ใช้ได้ไหมใครเป็นแบบนี้อยู่แล้ว
blockขึ้นบรรทัดใหม่เสมอ · กว้างเต็มพ่อได้div · p · h1
inlineไหลไปกับข้อความ ไม่ขึ้นบรรทัดใหม่ไม่ได้ — โดนเมินspan · a · strong
inline-blockไหลไปกับข้อความ แต่ตั้งขนาดได้ได้button · input · select · textarea
noneหายไปจากหน้าเว็บ ไม่กินที่เลยไม่มีความหมายไม่มี

ช่องขวาสุดของแถว inline-block มักถูกเขียนผิดว่า "ไม่มีใครเป็นโดยกำเนิด" — ผมอ่าน getComputedStyle().display ใน Chrome มาแล้ว: button · input · select · textarea (ของจากบทฟอร์ม) เป็น inline-block มาตั้งแต่แรก ส่วน img เป็น inline
ที่ต้องสั่งเองคือพวก div กับ span ธรรมดา ซึ่งไม่มีตัวไหนเกิดมาเป็น inline-block

<style>
  .tag { width: 120px; height: 40px;
         background: #ffd8a8; border: 1px solid #e8590c; }
  .a { display: inline; }
  .b { display: inline-block; }
  .c { display: block; }
  .d { display: none; }
</style>

<span class="tag a">inline</span>
<span class="tag b">inline-block</span>
<span class="tag c">block</span>
<span class="tag d">none</span>

ทั้งสี่กล่องสั่ง width: 120px; height: 40px; เหมือนกันเป๊ะ แต่ผลต่างกันสี่แบบ:
· inline — กล่องแรก แคบพอดีข้อความ ตัวเลข 120 กับ 40 ถูกเมินทั้งคู่ (ผมวัดกล่องนี้ในหน้านี้ได้ 39.89 × 20 คือความกว้างของข้อความ 37.89 บวก border 1px สองข้าง — กล่อง inline ยังรับ border กับ padding แนวนอนอยู่ ที่มันเมินคือ width กับ height เท่านั้น · ตัวเลขนี้ขึ้นกับฟอนต์ในเครื่องคุณ จะไม่ตรงเป๊ะก็ไม่แปลก — ที่ต้องตรงคือมันไม่ใช่ 120 × 40)
· inline-block — กล่องที่สอง อยู่บรรทัดเดียวกับตัวแรก และได้ขนาดตามสั่งจริง (วัดขอบนอกได้ 122 × 42 คือ 120 + 1×2 กับ 40 + 1×2 ที่บวก border เข้ามา — content-box จากหัวข้อ 4 กลับมาอีกครั้ง)
· block — กล่องที่สาม เด้งลงบรรทัดใหม่
· none — ไม่มีกล่องที่สี่ ทั้งที่เขียน <span> ไว้ใน HTML จริง ๆ

⚠️ display: none ต่างจาก "ซ่อนไว้"

display: none ทำให้ element หายไปจากการจัดหน้าโดยสิ้นเชิง ไม่กินที่แม้แต่ pixel เดียว ผมวัดของจริง: กล่องสูง 30px สามใบเรียงกัน ใบกลางสั่ง none → รวมสูง 60px ไม่ใช่ 90px
ที่ต้องระวังคือ โปรแกรมอ่านหน้าจอก็อ่านไม่เจอด้วย — ถ้าอยากซ่อนแค่ทางสายตาแต่ยังให้คนตาบอดเข้าถึงได้ none ไม่ใช่คำตอบ · และถ้าอยากให้มันหายแต่ยังกินที่เดิม ต้องใช้ visibility: hidden; แทน

🗣 ศัพท์ที่เพิ่งเรียนไป
block-level element
กล่องที่ขึ้นบรรทัดใหม่และกว้างเต็มพ่อ · ตั้ง width/height ได้ · ถาม senior ได้ว่า “ตัวนี้เป็น block หรือ inline อยู่” เป็นคำถามแรกที่ควรถามเวลา width ที่ใส่ไปแล้วไม่มีผล
inline element
กล่องที่ไหลไปกับข้อความ · เมิน width และ height · พิมพ์บอก AI ได้ว่า “this span is inline so width has no effect — should it be inline-block?” การชี้สาเหตุมาด้วยทำให้ได้คำตอบที่ใช้ได้เลย
inline-block
ลูกผสม — อยู่ในบรรทัดเดียวกับข้อความได้ แต่ตั้งขนาดได้เหมือน block · เป็นวิธีทำ badge หรือปุ่มเล็ก ๆ ที่แทรกในประโยค
display: none
เอา element ออกจากการจัดหน้าทั้งหมด · ต่างจาก visibility: hidden ที่ยังกินที่เดิมอยู่ · ถาม senior ได้ว่า “ตรงนี้ควรใช้ display:none หรือ visibility:hidden” คำตอบขึ้นกับว่าอยากให้ที่ว่างยุบตามหรือไม่

9 · สรุปเป็นลำดับการตัดสินใจ

อยากเว้นที่...
  |
  +-- ระหว่างเนื้อหากับขอบกล่องตัวเอง? ----> padding
  |
  +-- ระหว่างกล่องนี้กับกล่องอื่น? --------> margin  (ระวังยุบรวม)
  |                                          หรือ gap ถ้าพ่อเป็น flex/grid
  |
  +-- อยากได้เส้นกรอบด้วย? --------------> border  (อย่าลืม style)

ตั้ง width/height แล้วผลไม่ตรง...
  |
  +-- ล้นออกนอกพ่อ? --------------------> box-sizing: border-box
  |
  +-- ไม่มีผลเลย? ----------------------> element เป็น inline อยู่

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

1. .frame กว้าง 400px · ลูกข้างในเขียน width: 100%; padding: 16px; border: 2px solid black; โดยไม่ตั้ง box-sizing — กล่องลูกกว้างจริงเท่าไหร่?

ค่าเริ่มต้นของ CSS คือ box-sizing: content-box ซึ่งแปลว่า width นับเฉพาะชั้น content ส่วน padding กับ border ไปบวกข้างนอก:
400 + 16×2 + 2×2 = 436
ผมวัดชุดเดียวกันด้วย getBoundingClientRect() ใน Chrome จริง (กรอบ 300 · padding 20 · border 4) ได้ 348 ซึ่งตรงกับสูตร 300 + 40 + 8 เป๊ะ
ตัวเลือกที่ 1 คือกับดักตัวจริง เพราะเป็นสิ่งที่ทุกคนคาดหวังเวลาเขียน 100% — แต่มันคือพฤติกรรมของ border-box ซึ่งต้องสั่งเอง ไม่ได้มาให้ฟรี
ตัวเลือกที่ 2 ผิดตรงที่คิดว่าเบราว์เซอร์ช่วยกันล้นให้ — มันไม่ช่วยอะไรเลย ปล่อยล้นเงียบ ๆ ไม่มี warning
ตัวเลือกที่ 4 สลับบทบาทของ padding กับ border ซึ่งกลับด้านกัน — ในโหมด content-box บวกทั้งคู่
จุดที่ต้องจำ: width: 100% ในค่าเริ่มต้นไม่ได้แปลว่า "เต็มพ่อพอดี" แต่แปลว่า "content เต็มพ่อพอดี" — ทุกอย่างที่ห่ออยู่ข้างนอกจึงล้นออกไปหมด

2. div สองอันวางบนล่างติดกัน อันบนสั่ง margin-bottom: 40px อันล่างสั่ง margin-top: 24px — แล้วมีคนแก้ 24px เป็น 30px จะเกิดอะไรขึ้น?

ผมวัดของจริงใน Chrome ด้วย getBoundingClientRect(): ระยะจากขอบล่างกล่องบนถึงขอบบนกล่องล่าง = 40
เพราะ margin แนวตั้งที่มาชนกันยุบรวมเป็นก้อนเดียว แล้วใช้ max(40, 24) = 40
แก้ 24 เป็น 30 ก็ยังได้ max(40, 30) = 40 — ไม่ขยับ และต้องแก้จนเกิน 40 ถึงจะเห็นผล
ตัวเลือกที่ 1 คือสมมติฐานที่ทุกคนใช้ตอนแรก และเป็นเหตุผลที่คนนั่งงงอยู่นาน — ถ้ามันบวกกันจริง แก้ทีเดียวก็จบ
ตัวเลือกที่ 3 สับสนกับเรื่อง cascade (บรรทัดล่างทับบรรทัดบน) ซึ่งเป็นคนละเรื่อง — นี่เป็น property คนละตัวบน element คนละตัว ไม่มีอะไรทับอะไร
ตัวเลือกที่ 4 ผิดตรงที่คิดว่ากล่องล่างคุมฝ่ายเดียว — ทั้งคู่ส่งค่าเข้ามาแข่งกัน ตัวที่มากกว่าชนะ
ผมพิสูจน์อีกทางแล้วด้วย: เอาพ่อมาสั่ง display: flex; flex-direction: column; ระยะเดียวกันกลายเป็น 64 ทันที เพราะข้างในกล่อง flex ไม่มีการยุบรวม
จุดที่ต้องจำ: ถ้าแก้ตัวเลข margin แล้วหน้าจอไม่ขยับ อย่าเพิ่มตัวเลขมั่ว — ให้สงสัย margin collapsing ก่อน แล้วเปลี่ยนไปใช้ gap จบเรื่องเร็วกว่า

3. <html> เป็น 16px · .card ตั้ง font-size: 20px · ลูกใน .card ตั้ง font-size: 1.5em · ลูกของลูกตั้ง font-size: 1.5em อีกชั้น — ลูกของลูกได้กี่ px?

ผมอ่าน getComputedStyle().fontSize ของทั้งสามชั้นมาแล้ว: 20px → 30px → 45px
ชั้นแรก 1.5 × 20 = 30 · ชั้นสอง 1.5 × 30 = 45 — ไม่ใช่ 1.5 × 20 อีกครั้ง นี่คือความหมายของคำว่า "ทบต้น"
ตัวเลือกที่ 1 กับ 3 เอาพฤติกรรมของ rem มาใส่ em — rem ต่างหากที่อ้างจาก <html> เสมอ และในโจทย์นี้ 1.5rem จะได้ 24px (ผมวัดแล้วเช่นกัน)
ตัวเลือกที่ 2 คือคำตอบที่ถูกสำหรับชั้นเดียว — พอซ้อนชั้นที่สองมันก็ไม่ใช่แล้ว ซึ่งคือกับดักตัวจริง เพราะคนที่ทดสอบแค่ชั้นเดียวจะเข้าใจผิดว่าตัวเองเข้าใจถูก
จุดที่ต้องจำ: ในไฟล์ CSS คุณเห็นแค่ 1.5em เหมือนกันทุกชั้น ตัวเลขจริงจึงดูจากโค้ดไม่ออก ต้องเปิด DevTools อ่าน computed value — ถ้าไม่อยากเจอปัญหานี้ ใช้ rem เป็นค่าเริ่มต้น แล้วใช้ em เฉพาะที่ตั้งใจ

4. <span> ตัวหนึ่งเขียน width: 200px; height: 60px; background: orange; แต่บนหน้าจอมันแคบพอดีข้อความ — สาเหตุคืออะไร?

span · a · strong เป็น display: inline มาแต่กำเนิด และกล่อง inline ไม่รับ width กับ height — เบราว์เซอร์อ่านเจอแล้วเมินเงียบ ๆ
ผมวัดของจริงจากกล่องตัวอย่างในหัวข้อ 8 ของหน้านี้: span ที่สั่ง width: 120px; height: 40px ได้ขนาดจริง 39.89 × 20 — เท่าข้อความ (บวก border 1px สองข้าง) · พอเปลี่ยนเป็น display: inline-block ได้ 122 × 42 คือ 120 × 40 ตามสั่ง บวก border 1px สองข้าง
ตัวเลือกที่ 2 ไม่มีอยู่จริงใน CSS — element ไม่ได้เลือกหน่วยที่ตัวเองรับได้
ตัวเลือกที่ 3 สับสนกับ cascade เหมือนกัน — background กับ width เป็นคนละ property ไม่มีทางทับกันไม่ว่าจะเรียงลำดับยังไง
ตัวเลือกที่ 4 ฟังดูน่าเชื่อเพราะเพิ่งเรียน box-sizing ไป แต่ box-sizing แค่เปลี่ยนว่า width วัดถึงชั้นไหน ไม่ได้ทำให้ width เป็นศูนย์ — และมันไม่ช่วยอะไรเลยถ้ากล่องยังเป็น inline อยู่
จุดที่ต้องจำ: เจอ width ที่ "ใส่แล้วไม่มีอะไรเกิดขึ้น" ให้เช็ค display ก่อนเสมอ ก่อนจะไปสงสัยว่า selector ไม่โดนหรือค่าถูกทับ

🛠️ ภารกิจ — สร้างการ์ดหนึ่งใบ แล้ววัดมันด้วย DevTools

ภารกิจนี้ไม่ได้ให้ "ลองดูว่าสวยไหม" — ให้วัดตัวเลขออกมาแล้วเทียบกับที่คุณทำนายไว้ ซึ่งคือวิธีพิสูจน์ของฝั่ง CSS (แทนที่ของฝั่ง Go ที่ใช้เทส)
สร้างไฟล์ card.html ไฟล์เดียวจบ ไม่ต้องมี CSS แยก

  1. วางโครง: ทำ <div class="frame"> ที่ width: 360px และมี border: 2px dashed gray เพื่อให้เห็นขอบ
    ข้างในใส่ <div class="card"> ที่มีหัวเรื่อง (<h3>) กับย่อหน้า (<p>)
  2. ทำให้มันล้นก่อน: สั่ง .card ว่า width: 100%; padding: 24px; border: 3px solid teal;
    เขียนคำทำนายลงกระดาษก่อน ว่ากล่องจะกว้างกี่ px (ใช้สูตร content + padding×2 + border×2)
  3. วัดของจริง: เปิดไฟล์ในเบราว์เซอร์ → คลิกขวาที่การ์ด → Inspect → แท็บ Computed → เลื่อนลงล่างสุดจะเห็นภาพ box model ที่บอกตัวเลขทั้งสี่ชั้น
    ตัวเลขที่วัดได้ต้องตรงกับที่ทำนาย ถ้าไม่ตรง แปลว่าเข้าใจผิดตรงไหนสักที่ — หาให้เจอก่อนไปข้อถัดไป
  4. แก้ให้พอดี: เติม box-sizing: border-box; บรรทัดเดียว แล้ววัดซ้ำ — ต้องได้ 360 พอดี และช่องว่างข้างในต้องหดลง
    บันทึกตัวเลขก่อนแก้กับหลังแก้ไว้ทั้งคู่ นี่คือ "ผลเทส" ของคุณ
  5. ทำกับดักที่สองให้เกิด: ใช้การ์ดใบเดิม (ที่มี <h3> กับ <p> อยู่ข้างในแล้ว) เติมสองกฎนี้: h3 { margin-bottom: 32px; } กับ p { margin-top: 16px; }
    ทำนายก่อนว่าช่องว่างระหว่าง h3 กับ p จะกว้างกี่ px — คิดจากกติกาในหัวข้อ 5 ไม่ใช่ 32 + 16 — แล้ววัดด้วย DevTools ยืนยัน
    จากนั้นสั่ง .card { display: flex; flex-direction: column; } ทำนายตัวเลขใหม่ก่อน แล้วค่อยวัด — ตัวเลขต้องเปลี่ยน และต้องเปลี่ยนไปเป็นค่าที่คุณทายไว้
  6. ปิดท้าย: เปลี่ยน padding: 24px เป็น padding: 1.5rem แล้ววัดว่ายังได้ 24px เท่าเดิมไหม (16 × 1.5)
    แล้วลองเปลี่ยนเป็น 1.5em ดูว่าตัวเลขเปลี่ยนไหม และอธิบายให้ได้ว่าทำไม

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

อ้างอิง