🧰 ของที่ใช้ตรวจในบทนี้ (รันวันที่ 27 สิงหาคม 2026)
Chrome 148.0.7778.280 (macOS)
CSS Selectors Level 4 # นิยามของ :hover :focus :focus-visible :active
HTML Living Standard # นิยามของ :enabled / :disabled
กล่อง "ผลลัพธ์จริง" ทุกกล่องในบทนี้เอาโค้ดชุดเดียวกับที่โชว์ข้างบนไปเรนเดอร์จริง
บทนี้ต้องเอาเมาส์ไปชี้ ไปกด ไปกด Tab จริง ๆ ถึงจะได้ประโยชน์ —
อ่านผ่านตาอย่างเดียวจะไม่เห็นอะไรเลย เพราะทุกอย่างในบทนี้ซ่อนอยู่หลัง "การกระทำของผู้ใช้"
1 · State คืออะไร — element ตัวเดิม แต่หน้าตาไม่เหมือนเดิม
ลองนึกถึงปุ่ม "บันทึก" ปุ่มเดียวในหน้าเว็บ ปุ่มนั้นมีหลายช่วงชีวิต:
- ตอนอยู่เฉย ๆ — สีปกติ รอให้คนมาใช้
- ตอนเมาส์ลอยอยู่เหนือมัน — ควรเข้มขึ้นนิดหนึ่ง เพื่อบอกว่า "อันนี้กดได้นะ"
- ตอนคนกด Tab เดินมาถึงมัน — ควรมีกรอบล้อมรอบ เพื่อบอกว่า "ตอนนี้คุณอยู่ตรงนี้"
- ตอนกำลังกดค้างอยู่ — ควรยุบลงไปนิดหนึ่ง ให้รู้สึกว่ากดติดแล้ว
- ตอนถูกปิดไว้ (เช่นยังกรอกฟอร์มไม่ครบ) — ควรจางลง และกดไม่ได้จริง ๆ
ห้าอย่างนี้คือ element ตัวเดียวกัน ไม่ใช่ห้าปุ่ม
สิ่งที่ CSS ให้เรามาคือวิธีเขียนกฎแยกไว้คนละบรรทัด
แล้วเบราว์เซอร์จะสลับให้เองตามสิ่งที่ผู้ใช้ทำ เราไม่ต้องเขียนโค้ดสั่งสลับ
ปุ่มเรียกลิฟต์ในตึกเป็นปุ่มเดียว แต่มันบอกอะไรกับคุณหลายอย่าง:
ตอนปกติมันเป็นปุ่มเทา ๆ · ตอนคุณกดลงไปมันยุบลงและสว่างขึ้น
· ตอนลิฟต์กำลังซ่อมมันจะไม่สว่างเลยแม้จะกด
คุณไม่ต้องอ่านคู่มือ คุณรู้จากหน้าตาของมันล้วน ๆ ว่าตอนนี้มันอยู่ในสถานะไหน
CSS state ก็ทำงานแบบเดียวกัน: เปลี่ยนหน้าตา = สื่อสารสถานะโดยไม่ต้องมีตัวหนังสือ
ที่ไม่เหมือนกัน: ปุ่มลิฟต์รู้ตัวเองว่าลิฟต์เสีย เพราะสายไฟต่อถึงกัน
แต่ปุ่มใน CSS ไม่รู้อะไรเลยเกี่ยวกับระบบของคุณ —
มันจางลงเพราะคุณ (หรือ JavaScript ของคุณ) ไปแปะคำว่า disabled ให้มันเท่านั้น
ถ้าลืมแปะ ปุ่มจะสดใสเหมือนเดิมทั้งที่ระบบพังไปแล้ว
ลองของจริงเลย ปุ่มสองปุ่มข้างล่างนี้เขียนด้วย CSS ล้วน ไม่มี JavaScript
เอาเมาส์ไปชี้ · กดค้าง · แล้วลองคลิกในกล่องแล้วกด Tab
<style>
.btn {
font: inherit;
padding: 10px 20px;
border: 0;
border-radius: 8px;
background: #b49a9c;
color: #ffffff;
cursor: pointer;
}
.btn:hover { background: #96797c; }
.btn:active { background: #6f5559; }
.btn:focus-visible { outline: 3px solid #1a73e8; outline-offset: 2px; }
.btn:disabled { background: #e4e4e4; color: #a0a0a0; cursor: not-allowed; }
</style>
<button class="btn">ปุ่มที่ใช้งานได้</button>
<button class="btn" disabled>ปุ่มที่ถูกปิดไว้</button>
สังเกตว่าเราเขียนกฎ 4 บรรทัดเพิ่มจากของเดิม แล้วได้พฤติกรรมครบทั้งชุด
สิ่งที่ทำให้มันเป็นไปได้คือเครื่องหมาย : ที่ต่อท้าย selector —
หัวข้อถัดไปเราจะไล่ทีละตัวว่าแต่ละตัวจับ "ช่วงเวลา" ไหน
- pseudo-class (ซูโด-คลาส)
-
ส่วนที่ต่อท้าย selector ด้วย
:เช่น:hover:disabledแปลว่า "เลือก element นี้ เฉพาะตอนที่ เงื่อนไขนี้เป็นจริง" คำว่า pseudo แปลว่า "เสมือน" — มันทำตัวเหมือน class ที่เบราว์เซอร์แอบแปะให้เองแล้วดึงออกเอง โดยที่ HTML ของเราไม่ต้องเปลี่ยนเลย
พิมพ์ถาม AI ได้ว่า: "ใน CSS มี pseudo-class อะไรบ้างที่ใช้กับ state ของ form control และตัวไหนใช้กับ element ทั่วไปได้" - state (ของ UI)
-
สถานะที่ component หนึ่งกำลังอยู่ ณ ตอนนั้น — enabled · hover · focus · pressed · disabled
เป็นคำที่ทีม design ใช้กันมากกว่าคำว่า pseudo-class
ถาม senior ได้ว่า: "ปุ่มในดีไซน์ตัวนี้ ทีม design กำหนดสีของ state ไหนไว้บ้าง มีสีของ pressed กับ disabled ให้ไหม หรือให้ผมคิดเอง"
2 · :hover — ตอนที่เมาส์ลอยอยู่เหนือมัน
:hover เป็นจริงตั้งแต่วินาทีที่ตัวชี้เมาส์เข้ามาอยู่ในพื้นที่ของ element
และเป็นเท็จทันทีที่เมาส์ออกไป ไม่เกี่ยวกับการคลิกเลย
ใช้กับ element อะไรก็ได้ ไม่ใช่แค่ปุ่ม — <div> <tr>
<li> ก็ hover ได้หมด
<style>
.row {
padding: 10px 14px;
border-bottom: 1px solid #e0e0e0;
transition: background 0.15s;
}
.row:hover { background: #f3ece9; }
</style>
<div class="row">ผัดกะเพราหมูสับ</div>
<div class="row">ต้มยำกุ้งน้ำข้น</div>
<div class="row">ข้าวมันไก่ต้ม</div>
transition ที่ใส่ไว้ทำให้สีค่อย ๆ เปลี่ยนแทนที่จะกระโดด —
ไม่ใส่ก็ทำงานได้ แต่ใส่แล้วรู้สึกนุ่มขึ้นมาก และต้องใส่ที่กฎปกติ ไม่ใช่ที่ :hover
ไม่งั้นตอนเมาส์ออกจะกระตุกกลับทันที
โทรศัพท์ไม่มีตัวชี้ที่ "ลอยอยู่เหนือ" อะไรได้ มีแต่นิ้วที่แตะหรือไม่แตะ
ผลคือ ถ้าคุณเอาข้อมูลที่จำเป็นไปซ่อนไว้หลัง :hover
เช่นปุ่มลบที่โผล่มาตอน hover · ราคาที่โผล่มาตอน hover · คำอธิบายที่โผล่มาตอน hover
→ คนใช้มือถือจะไม่มีวันเห็นมันเลย
ไม่ใช่ "เห็นยาก" แต่คือไม่มีทางเห็น
กฎที่ปลอดภัย: :hover ใช้ตกแต่งได้ ใช้ซ่อนของไม่ได้
ถ้าเนื้อหาชิ้นไหนหายไปเมื่อไม่ hover แล้วผู้ใช้ทำงานต่อไม่ได้ แปลว่าคุณใช้ผิดที่
ถ้าอยากเขียนสไตล์ที่ เฉพาะเครื่องที่มีเมาส์จริงเท่านั้นถึงจะใช้ ให้ครอบด้วย media query ที่ถามเรื่องความสามารถของอุปกรณ์ ไม่ใช่ถามความกว้างจอ:
/* ใช้กฎนี้เฉพาะเครื่องที่มีตัวชี้ซึ่ง hover ได้จริง (เมาส์ / trackpad) */
@media (hover: hover) {
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.18); }
}
ทำไมต้องถาม (hover: hover) แทนที่จะถามความกว้างจอ?
เพราะจอกว้างไม่ได้แปลว่ามีเมาส์ — แท็บเล็ตจอใหญ่ก็ไม่มี hover
ส่วนหน้าต่างเบราว์เซอร์ที่ย่อแคบ ๆ บนเครื่อง desktop ก็ยังมีเมาส์อยู่
ถามให้ตรงกับสิ่งที่อยากรู้เสมอ
3 · :focus กับ :focus-visible — จุดที่คนทำเว็บพังกันมากที่สุด
focus แปลว่า "ตอนนี้คีย์บอร์ดคุยอยู่กับ element ตัวนี้"
ในหน้าเว็บหนึ่งหน้า มี element ที่ได้ focus ได้แค่ตัวเดียวในเวลาเดียวกัน
ถ้าคุณพิมพ์ตัวอักษร มันจะไปโผล่ที่ช่องที่ focus อยู่ ถ้าคุณกด Enter มันจะกดปุ่มที่ focus อยู่
วิธีย้าย focus มีหลายทาง: คลิก · กด Tab ไปข้างหน้า · Shift+Tab ถอยหลัง
· สั่งด้วยเสียง · หรือใช้ปุ่มบนอุปกรณ์ช่วยเหลือ
ห้องประชุมมีไมค์ไร้สายอยู่ตัวเดียว ใครถือไมค์อยู่ คำพูดของคนนั้นจะออกลำโพง
คนที่ถือไมค์ = element ที่ได้ focus · เสียงที่ออกลำโพง = ตัวอักษรที่พิมพ์
และเหตุผลที่ห้องประชุมดี ๆ ต้องเห็นได้ชัดว่าใครถือไมค์อยู่
ก็คือเหตุผลเดียวกับที่ element ที่ focus ต้องมีกรอบให้เห็น
ที่ไม่เหมือนกัน: ในห้องประชุมคุณมองเห็นคนที่ถือไมค์ได้เองด้วยตา
แต่ในหน้าเว็บ ถ้าคุณลบกรอบ focus ทิ้ง มันจะไม่มีร่องรอยอะไรเหลือให้ดูเลย
คนที่ใช้คีย์บอร์ดจะกด Tab ไปเรื่อย ๆ โดยไม่รู้ว่าตัวเองอยู่ตรงไหนของหน้า
ทีนี้ปัญหาจริงที่ทำให้เกิด :focus-visible ขึ้นมา:
| สถานการณ์ | :focus | :focus-visible |
|---|---|---|
| กด Tab มาถึงปุ่ม | ตรง ✅ | ตรง ✅ |
| คลิกปุ่มด้วยเมาส์ | ตรง ✅ (กรอบที่คุณเขียนไว้ที่ :focus จะค้างอยู่) | ไม่ตรง — เบราว์เซอร์ถือว่าไม่ต้องโชว์กรอบ |
| คลิกในช่องกรอกข้อความ | ตรง ✅ | ตรง ✅ (เพราะต้องเห็นว่าพิมพ์ตรงไหน) |
แถวกลางคือหัวใจ — สมัยก่อนที่มีแต่ :focus ทุกครั้งที่ผู้ใช้เมาส์คลิกปุ่ม
จะมีกรอบดำ ๆ ค้างอยู่รอบปุ่มจนกว่าจะไปคลิกที่อื่น นักออกแบบเกลียดมันมาก
วิธีที่คนแก้กันคือเขียนบรรทัดนี้ — และมันคือบรรทัดที่ทำลาย accessibility ของเว็บมากที่สุดในประวัติศาสตร์:
outline: none เปล่า ๆ
/* ห้ามเขียนแบบนี้เด็ดขาด */
button:focus { outline: none; }
บรรทัดนี้ลบกรอบทิ้งทุกกรณี รวมทั้งกรณีที่คนกด Tab มา
ผลที่ตามมาไม่ใช่ "หน้าเว็บสะอาดขึ้น" แต่คือ คนที่ใช้คีย์บอร์ดอย่างเดียวใช้เว็บของคุณไม่ได้อีกต่อไป
เขาจะกด Tab แล้วไม่รู้ว่าตอนนี้อยู่ที่ปุ่มไหน กด Enter ไปก็ไม่รู้ว่ากดอะไร
คนกลุ่มนี้ไม่ได้มีแค่ผู้พิการ — รวมถึงคนที่เมาส์เสีย คนที่ใช้ laptop บนรถไฟ
และคนที่พิมพ์เร็วจนไม่อยากละมือจากคีย์บอร์ด
ทางแก้ที่ถูกคือ อย่าลบ — ให้เปลี่ยนแทน โดยย้ายกรอบไปไว้ที่ :focus-visible
ซึ่งเบราว์เซอร์จะเปิดให้เฉพาะตอนที่มัน "คิดว่าผู้ใช้ต้องการเห็นกรอบ" (คือตอนใช้คีย์บอร์ด)
<style>
.pill {
font: inherit;
padding: 10px 18px;
margin-right: 8px;
border: 1px solid #c9bcbd;
border-radius: 999px;
background: #ffffff;
cursor: pointer;
}
/* ✅ ถูก: ลบของ default แล้ววาดของเราแทน ในเงื่อนไขที่ควรเห็น */
.pill:focus { outline: none; }
.pill:focus-visible { outline: 3px solid #1a73e8; outline-offset: 3px; }
/* ❌ ผิด: ลบทิ้งเฉย ๆ ไม่วาดอะไรกลับมา */
.ghost:focus { outline: none; }
</style>
<button class="pill">ปุ่มที่ทำถูก</button>
<button class="pill ghost">ปุ่มที่ทำผิด</button>
ลองเองตรงนี้: คลิกในกล่องผลลัพธ์ข้างบนหนึ่งครั้ง แล้วกด Tab ไปเรื่อย ๆ
ปุ่มซ้ายจะมีวงแหวนสีน้ำเงินโผล่มาให้เห็นชัดว่าคุณอยู่ตรงไหน
ส่วนปุ่มขวา — คุณจะรู้ว่าตัวเองยืนอยู่บนมันก็ต่อเมื่อกด Enter แล้วมีอะไรเกิดขึ้นเท่านั้น
ความรู้สึกอึดอัดตอนนั้น คือความรู้สึกที่ผู้ใช้จริงเจอทุกหน้าที่คุณเขียน outline: none ทิ้งไว้
ต้องเขียน :focus { outline: none } ด้วยไหม ในเมื่อจะวาดกรอบใหม่อยู่แล้ว?
ผมลองวัดในเบราว์เซอร์ที่ใช้ตรวจบทนี้: เขียนแค่ :focus-visible { outline: 3px solid blue }
อย่างเดียว ไม่เขียน :focus เลย แล้วกด Tab มาที่ปุ่ม
ค่าที่อ่านได้จาก getComputedStyle คือ rgb(0, 0, 255) solid 3px —
กรอบของเราทับกรอบ default ไปเลย ไม่ได้ซ้อนกันสองชั้น
เพราะ outline เป็น property เดียว element ตัวหนึ่งจึงมีได้ค่าเดียว
บรรทัด :focus { outline: none } จึงเป็นบรรทัดกันเหนียว
ไว้กวาดกรอบ default ทิ้งในเบราว์เซอร์รุ่นที่ยังวาดกรอบนั้นที่ :focus เฉย ๆ
(ไม่ใช่ที่ :focus-visible) — เขียนไว้ไม่เสียหาย แต่ถ้าเขียนต้องระวังลำดับ
เขียน :focus ก่อน :focus-visible เสมอ
เพราะสองอันนี้ specificity เท่ากัน ตัวที่เขียนทีหลังจะชนะ —
ถ้าสลับลำดับ กรอบสวย ๆ ของคุณจะโดน none ทับหายไปทั้งหมด
- focus ring / focus indicator
-
กรอบที่ล้อมรอบ element ที่กำลังได้ focus — ในโค้ดคือ property
outline
ข้อดีของoutlineเมื่อเทียบกับborderคือ มันไม่กินพื้นที่ในการจัดวาง ปุ่มจึงไม่ขยับตอนกรอบโผล่มา ส่วนoutline-offsetคือระยะห่างระหว่างกรอบกับตัว element
ถาม senior ได้ว่า: "โปรเจกต์เรามี focus ring กลางไหม หรือให้ผมเขียน:focus-visibleเองในแต่ละ component" - keyboard navigation / tab order
-
การใช้เว็บด้วยคีย์บอร์ดล้วน โดยกด Tab ไล่ไปทีละ element ที่โฟกัสได้
ลำดับที่ไล่คือลำดับที่ element เรียงอยู่ใน HTML ไม่ใช่ลำดับที่ตาเห็นบนจอ
(นี่คือเหตุผลหนึ่งที่ทำให้การใช้ CSS ย้ายของไปมามาก ๆ เป็นเรื่องอันตราย)
พิมพ์บอก AI ได้ว่า: "ช่วยตรวจหน้านี้ให้หน่อยว่า tab order เดินตามลำดับที่ผู้ใช้เห็นบนจอหรือเปล่า และมี element ไหนที่โฟกัสได้แต่ไม่มี focus indicator" - WCAG
-
ชื่อย่อของมาตรฐาน Web Content Accessibility Guidelines ของ W3C
เป็นเอกสารที่บอกว่า "เว็บที่คนทุกกลุ่มใช้ได้ ต้องทำอะไรบ้าง"
ข้อ 2.4.7 Focus Visible คือข้อที่พูดถึงกรอบ focus โดยตรง
และหลายองค์กร (รวมถึงงานราชการหลายประเทศ) บังคับตามมาตรฐานนี้ในสัญญาจ้าง
ถาม senior ได้ว่า: "โปรเจกต์นี้ต้องผ่าน WCAG ระดับไหน AA หรือ AAA มี checklist ที่ทีมใช้ตรวจก่อนส่งงานไหม"
4 · :active — ตอนกำลังกดค้างอยู่
:active เป็นจริงในช่วงเวลาสั้น ๆ คือตั้งแต่ปุ่มเมาส์กดลง จนถึงตอนปล่อย
สไลด์เรียก state นี้ว่า Pressed ซึ่งเป็นคำที่ทีม design ใช้
ส่วนใน CSS มันชื่อ :active — คนละคำ ของอันเดียวกัน จำไว้ว่าเวลาคุยกับ designer
เขาจะพูดว่า "pressed" ไม่ใช่ "active"
<style>
.push {
font: inherit;
padding: 12px 24px;
border: 0;
border-radius: 8px;
background: #b49a9c;
color: #ffffff;
cursor: pointer;
}
.push:active { background: #6f5559; }
.push:hover { background: #96797c; }
</style>
<button class="push">กดค้างที่ปุ่มนี้</button>
โค้ดนี้เขียน :active ไว้ก่อน :hover
ตอนคุณเอาเมาส์ไปกดปุ่มค้างไว้ ทั้งสองเงื่อนไขเป็นจริงพร้อมกัน (เมาส์อยู่เหนือปุ่ม + กำลังกด)
คิดว่าปุ่มจะเป็นสีอะไร — สีเข้มสุดของ :active หรือสีกลางของ :hover? เพราะอะไร?
ได้ สีกลางของ :hover — สีเข้มของ :active ไม่มีวันโผล่ตอนกดด้วยเมาส์
เหตุผล: .push:active กับ .push:hover มี specificity
เท่ากันเป๊ะ (class หนึ่งตัว + pseudo-class หนึ่งตัว เท่ากันทั้งคู่)
เมื่อ specificity เท่ากัน CSS ตัดสินด้วยกฎเดียว: ตัวที่เขียนทีหลังชนะ
ตอนกดค้าง ทั้งสองกฎเป็นจริงพร้อมกัน :hover ที่อยู่บรรทัดล่างจึงทับ :active
วิธีแก้คือสลับลำดับให้ :active อยู่ล่างสุด
ลำดับที่ปลอดภัยสำหรับปุ่มทั่วไปคือ :hover → :focus-visible → :active
ส่วนลิงก์ <a> มีลำดับที่ต้องท่องเพิ่มคือ
LVHA = :link → :visited → :hover → :active
(คนจำกันว่า "LoVe—HAte") เพราะถ้าเอา :visited ไว้ล่าง
ลิงก์ที่เคยเข้าแล้วจะไม่เปลี่ยนสีตอน hover เลย
โค้ดชุดเดียวกันนี้เอาไปเรนเดอร์จริงแล้วเป็นแบบนี้ — ลองกดค้างดูเอง จะเห็นว่ามันไม่เข้มสุดจริง ๆ
5 · :disabled และ :enabled — ปิดจริง ไม่ใช่แค่ทำให้จาง
disabled เป็นattribute ใน HTML ไม่ใช่ CSS
พอแปะเข้าไป เบราว์เซอร์จะทำสามอย่างให้ทันทีโดยเราไม่ต้องเขียนอะไรเลย:
- กดแล้วไม่มีอะไรเกิดขึ้น — event
clickไม่ยิงออกมาเลย - กด Tab ข้ามมันไป — โฟกัสไม่เข้า
- ถ้าอยู่ในฟอร์ม ค่าของมันจะไม่ถูกส่งไปกับฟอร์ม
ส่วน :disabled กับ :enabled ใน CSS คือ pseudo-class ที่ตามหาว่าตัวไหนถูกปิดอยู่
เพื่อให้เราเปลี่ยนหน้าตาตาม
การใส่ attribute disabled = ล็อกประตูจริง ๆ ผลักยังไงก็ไม่เปิด
การเขียน CSS ให้ปุ่มจางลงเฉย ๆ = แค่แปะป้าย "ปิดปรับปรุง" ไว้หน้าประตูที่ไม่ได้ล็อก
ดูเหมือนปิด แต่ใครผลักก็เข้าได้อยู่ดี
ของจริงต้องทำทั้งสองอย่าง: ล็อกด้วย attribute (เพื่อความถูกต้อง) และแปะป้ายด้วย CSS (เพื่อให้คนเห็น)
ที่ไม่เหมือนกัน: ประตูที่ล็อกอยู่ ต่อให้ไม่มีป้าย คนก็รู้ทันทีที่ผลักแล้วไม่เปิด
แต่ปุ่มที่ disabled โดยไม่ทำให้จาง ผู้ใช้จะกดแล้วกดอีกหลายรอบ
เพราะไม่มีอะไรบอกว่ามันพัง เขาจะคิดว่าเว็บของคุณค้าง
ข้อควรระวังที่คนไม่ค่อยรู้: :enabled กับ :disabled
ใช้ได้กับ form control เท่านั้น ผมลองในเบราว์เซอร์แล้วได้ผลนี้:
ทดสอบด้วย el.matches(…) | ผลจริงใน Chrome 148 |
|---|---|
<input> ธรรมดา ตรงกับ :enabled ไหม | true |
<div> ตรงกับ :enabled ไหม | false |
<a href="#"> ตรงกับ :enabled ไหม | false |
<button> ที่อยู่ใน <fieldset disabled> ตรงกับ :disabled ไหม | true — ปิดยกชุดได้ |
เรียก .click() ใส่ปุ่มที่ disabled — handler ยิงกี่ครั้ง | 0 ครั้ง (ปุ่มปกติได้ 1) |
แถวสุดท้ายคือหลักฐานว่า disabled ปิดของจริง ไม่ใช่แค่หน้าตา —
แม้แต่โค้ดที่สั่งกดตรง ๆ ก็ยังไม่ผ่าน
ส่วนสองแถวกลางแปลว่า ถ้าคุณทำ "ปุ่ม" ด้วย <div> คุณจะไม่ได้อะไรจาก
disabled เลย — เป็นอีกหนึ่งเหตุผลที่ปุ่มควรเป็น <button> จริง ๆ
<style>
input, button {
font: inherit;
padding: 8px 12px;
border: 1px solid #c9bcbd;
border-radius: 6px;
}
input:enabled { background: #ffffff; }
input:disabled { background: #f0f0f0; color: #9a9a9a; cursor: not-allowed; }
/* ปิดยกชุดด้วย fieldset ตัวเดียว — ของข้างในโดนหมด */
fieldset { border: 1px dashed #c9bcbd; border-radius: 8px; }
fieldset:disabled { opacity: 0.6; }
</style>
<input value="ช่องนี้พิมพ์ได้">
<input value="ช่องนี้ถูกปิด" disabled>
<fieldset disabled>
<legend>ปิดทั้งกล่องด้วย fieldset</legend>
<input value="ตัวนี้ไม่ได้แปะ disabled เอง">
<button>แต่ก็กดไม่ได้</button>
</fieldset>
ลองกด Tab ไล่ในกล่องข้างบนดู — คุณจะข้ามช่องที่ปิดไว้ไปเองโดยอัตโนมัติ
และเข้าไปในกรอบ fieldset ไม่ได้เลย ทั้งที่ข้างในไม่ได้แปะ disabled ทีละตัว
- form control
-
ชื่อรวมของ element ที่ผู้ใช้ "กรอกหรือสั่ง" ได้ —
<input><button><select><textarea><fieldset><option>
เป็นเส้นแบ่งที่สำคัญมาก เพราะ pseudo-class หลายตัว (:disabled·:enabled·:checked·:required) ทำงานเฉพาะกับกลุ่มนี้ ถ้าเอาไปใช้กับ<div>มันจะไม่ error แต่จะเงียบไปเฉย ๆ
พิมพ์บอก AI ได้ว่า: "ช่วยดูหน่อยว่าในหน้านี้มีที่ไหนที่ผมทำ interactive element ด้วย<div>ทั้งที่ควรใช้ form control จริง ๆ" - attribute กับ pseudo-class — คนละชั้นกัน
-
disabled(ไม่มี:) คือ attribute ใน HTML — มันเปลี่ยนพฤติกรรมจริง ๆ ของ element:disabled(มี:) คือ pseudo-class ใน CSS — มันแค่เลือก element ที่มี attribute นั้นมาแต่งหน้าตา ไม่ได้ปิดอะไรเลย
คนใหม่มักคิดว่าเขียน CSS แล้วปุ่มจะกดไม่ได้ ซึ่งไม่จริง — ต้องแปะ attribute ด้วยเสมอ
ถาม senior ได้ว่า: "ปุ่มนี้ควรปิดด้วย attributedisabledหรือควรให้กดได้แล้วขึ้น error message ดี ผู้ใช้แบบไหนจะสับสนน้อยกว่ากัน"
6 · "Dragged" ในสไลด์ — state ที่ CSS ไม่มีให้
สไลด์หน้า 63 ใส่ state ชื่อ Dragged (ตอนผู้ใช้กดแล้วลากของ) ไว้ในชุดเดียวกับตัวอื่น
แต่ถ้าคุณไปหา :dragged ใน CSS คุณจะไม่เจอ เพราะมันไม่มีอยู่จริง
รายการ state ในสไลด์ (enabled · hover · focused · pressed · disabled · dragged)
ไม่ใช่รายการ pseudo-class ของ CSS — มันคือรายการ state ในภาษาของ
design system ซึ่ง Material Design 3 ใช้คำชุดนี้เป๊ะ ๆ
ห้าตัวแรก (enabled · hover · focused · pressed · disabled) มี pseudo-class ให้ใช้ตรงตัว —
:enabled · :hover · :focus · :active · :disabled
ตามลำดับ — ส่วนตัวสุดท้ายคือ dragged ไม่มี
ผมลองถามเบราว์เซอร์ตรง ๆ ด้วย CSS.supports('selector(:dragged)')
ได้ false · ส่วน :-webkit-drag ได้ true
แต่ตัวนั้นขึ้นต้นด้วย -webkit- แปลว่าเป็นของเฉพาะเบราว์เซอร์ตระกูลเดียว
ไม่ใช่มาตรฐาน อย่าเอาไปใช้ในงานจริง
ในทางปฏิบัติ สิ่งที่ทำได้ด้วย CSS ล้วนคือบอกใบ้ด้วยรูปตัวชี้เมาส์:
cursor: grab แปลว่า "จับได้นะ" และ cursor: grabbing แปลว่า "กำลังจับอยู่"
ซึ่งจับคู่กับ :active ได้พอดี
<style>
.chip {
display: inline-block;
padding: 12px 20px;
border-radius: 10px;
background: #b49a9c;
color: #ffffff;
cursor: grab; /* มือเปิด = ลากได้ */
user-select: none;
}
.chip:active {
cursor: grabbing; /* มือกำ = กำลังลาก */
box-shadow: 0 6px 14px rgba(0,0,0,0.25);
}
</style>
<div class="chip">กดค้างที่การ์ดนี้แล้วดูตัวชี้เมาส์</div>
ส่วน "การลากของจริง ๆ ให้ย้ายที่ได้" ต้องใช้ JavaScript เข้ามาช่วย
โดยตอนเริ่มลากก็แปะ class เช่น .is-dragging เข้าไปเอง แล้วเขียน CSS รับที่ class นั้น
เรื่องนี้อยู่นอกขอบเขตบทนี้ แต่รู้ไว้ว่าทุกครั้งที่ design system พูดถึง state
ที่ CSS ไม่มีให้ วิธีแก้คือแปะ class เอง — ไม่ใช่ไปหา pseudo-class แปลก ๆ มาใช้
7 · สรุปรวม — เขียนเรียงลำดับยังไงถึงไม่ทับกันเอง
| เขียน | เป็นจริงตอน | ใช้กับ | ต้องระวัง |
|---|---|---|---|
:hover |
ตัวชี้ลอยอยู่เหนือ | element อะไรก็ได้ | มือถือไม่มี — ห้ามซ่อนของสำคัญไว้หลังมัน |
:focus |
คีย์บอร์ดคุยอยู่กับตัวนี้ (คลิกก็นับ) | ตัวที่โฟกัสได้ | อย่าใส่ outline: none แล้วจบแค่นั้น |
:focus-visible |
โฟกัสอยู่ และ เบราว์เซอร์เห็นว่าควรโชว์กรอบ | ตัวที่โฟกัสได้ | ต้องเขียนหลัง :focus ไม่งั้นโดนทับ |
:active |
กำลังกดค้างอยู่ (designer เรียก pressed) | element อะไรก็ได้ | ต้องเขียนไว้ล่างสุด ไม่งั้น :hover ทับ |
:disabled / :enabled |
มี / ไม่มี attribute disabled |
form control เท่านั้น | <div> กับ <a> ใช้ไม่ได้ |
ปุ่มทั่วไป: ปกติ → :hover → :focus-visible → :active → :disabled
ลิงก์ <a>: LVHA — :link → :visited → :hover → :active
เหตุผลเดียวกันทั้งคู่: pseudo-class พวกนี้ specificity เท่ากันหมด ตัวล่างสุดจึงชนะเสมอ
เรียงจาก "เกิดบ่อย/ถาวร" ไปหา "เกิดชั่วขณะ" แล้วจะไม่มีวันทับกันผิด
สร้างไฟล์ states.html ไฟล์เดียวจบ ไม่ต้องมี JavaScript
เป้าหมายไม่ใช่ "ทำให้สวย" แต่คือ ทำให้ใช้ได้โดยไม่ต้องแตะเมาส์เลย
- ทำปุ่ม 3 ปุ่ม ที่ใช้ class
.btnเหมือนกัน — ปุ่มที่ 3 ใส่ attributedisabled - เขียน state ให้ครบ 4 อย่าง ตามลำดับในตารางข้างบน:
:hoverเปลี่ยนสีพื้น ·:focus-visibleให้outlineหนาอย่างน้อย 2px ·:activeเปลี่ยนสีเข้มกว่า hover ·:disabledจางลงและcursor: not-allowed - เพิ่ม
@media (hover: hover)ครอบกฎ:hoverไว้ - ทดสอบด้วยคีย์บอร์ดล้วน — ห้ามแตะเมาส์:
เอามือออกจากเมาส์จริง ๆ แล้วกด Tab ตั้งแต่บนสุดของหน้า
เกณฑ์ผ่าน: ทุกครั้งที่กด Tab คุณต้องบอกได้ทันทีว่าตอนนี้อยู่ที่ปุ่มไหน โดยไม่ต้องเดา และปุ่มที่ 3 ต้องถูกข้ามไปเอง
ถ้ามีจังหวะไหนที่คุณไม่รู้ว่าตัวเองอยู่ตรงไหน = ยังไม่ผ่าน กลับไปดูข้อ 2 - ทดสอบตรงข้าม เพื่อพิสูจน์ว่า
:focus-visibleทำงานจริง: คราวนี้คลิกปุ่มที่ 1 ด้วยเมาส์
เกณฑ์ผ่าน: ต้องไม่มีกรอบค้างอยู่หลังคลิก (ถ้ามีกรอบค้าง แปลว่าคุณเขียนที่:focusไม่ใช่:focus-visible) - ทดสอบว่า disabled ปิดของจริง: ลองกดปุ่มที่ 3 รัว ๆ แล้วเปิด DevTools ดูว่ามี event อะไรเกิดขึ้นไหม — ควรจะเงียบสนิท
โจทย์เสริมสำหรับคนที่เสร็จเร็ว: ลองสลับให้ :hover อยู่ล่าง :active
แล้วกดค้างดู — คุณควรทำให้สีของ :active หายไปได้ตามที่ predict ในหัวข้อ 4 อธิบายไว้
ถ้าทำให้มันพังตามที่คาดได้ แปลว่าคุณเข้าใจเรื่องลำดับจริง ไม่ใช่แค่ท่องจำ
8 · ที่ฝึกต่อ และคู่มือที่ควรรู้ว่ามีอยู่
สไลด์ปิดท้ายด้วยลิงก์สองชุด ผมแยกให้ชัดว่าชุดไหนใช้ทำอะไร เพราะสองชุดนี้คนละหน้าที่กันเลย
ชุดที่ 1 — เกมฝึกมือ (เล่นตอนอยากได้ความคล่อง)
| ที่ไหน | ฝึกอะไร | ควรเล่นตอนไหน |
|---|---|---|
| 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 (และภาษาอื่น) เป็นอาวุธ | เล่นเอาสนุก · ไม่ต้องรีบ ไม่ใช่ทางลัดของเรื่องไหนโดยเฉพาะ |
ชุดที่ 2 — คู่มือที่เอาไว้เปิดตอนต้องตัดสินใจ
สี่อันนี้ไม่ใช่ของที่อ่านรวดเดียวจบ — ไม่มีใครอ่านหมด และไม่ต้องอ่านหมด
หน้าที่ของมันคือ "เวลาไม่แน่ใจว่าควรทำยังไง ให้เปิดดูว่าคนที่คิดเรื่องนี้มาสิบปีแล้วเขาสรุปว่าไง"
| คู่มือ | เอาไว้ตอบคำถามแบบไหน | เข้าไปแล้วดูตรงไหนก่อน |
|---|---|---|
| CSS-Tricks Guides | "property นี้มันทำงานยังไงกันแน่" | หน้า A Complete Guide to Flexbox — เป็นหน้าที่คนทำเว็บทั้งโลกเปิดบ่อยที่สุดหน้าหนึ่ง |
| Material Design 3 | "ปุ่มตอน hover ควรเข้มขึ้นเท่าไหร่ ตอน pressed ควรเป็นยังไง" | หัวข้อ Foundations → Interaction states — ที่มาของรายการ state ในสไลด์หน้า 58–64 พอดี |
| Apple HIG | "ปุ่มควรใหญ่แค่ไหนถึงกดโดนบนมือถือ" | หัวข้อ Inputs และ Foundations → Accessibility |
| WCAG 2.1 | "เว็บนี้คนที่มองไม่เห็น / ใช้เมาส์ไม่ได้ ใช้ได้ไหม" | ข้อ 2.4.7 Focus Visible ข้อเดียวก่อน — สั้นมาก และมันคือหัวข้อ 3 ของบทนี้ทั้งดุ้น |
💬 ถามกลับได้ตลอด — ถ้าอ่านแล้วยังไม่แน่ใจว่า
:focus กับ :focus-visible ต่างกันตรงไหนกันแน่
หรือทำภารกิจแล้วกรอบไม่โผล่สักที ให้ถามมาพร้อมโค้ดที่คุณเขียน
คำถามที่ตอบง่ายที่สุดคือ "ผมเขียนแบบนี้ คาดว่าจะได้ X แต่ได้ Y" —
ถ้าบอกทั้งสิ่งที่คาดและสิ่งที่ได้ จะหาสาเหตุเจอเร็วกว่าถามว่า "ทำไมไม่ทำงาน" มาก
9 · ทดสอบความเข้าใจ
1. ทีมออกแบบสั่งว่า "ราคาสินค้าให้โผล่มาตอนเอาเมาส์ชี้ที่การ์ด เพื่อให้หน้าเว็บดูสะอาด" — ควรตอบเขายังไง?
ราคาคือข้อมูลที่ผู้ใช้ต้องมีเพื่อตัดสินใจ ไม่ใช่ของตกแต่ง —
เอาไปซ่อนหลัง :hover แปลว่าคนใช้มือถือซื้อของไม่ได้
ตัวเลือกที่ 1 แก้ผิดจุด: @media (hover: hover) ทำให้มือถือไม่ได้กฎ hover
ซึ่งแปลว่าราคาถูกซ่อนถาวรยิ่งกว่าเดิม ไม่ได้ทำให้มันโผล่มา
ตัวเลือกที่ 2 ผิดที่ข้อเท็จจริง — บางเบราว์เซอร์บนมือถือแกล้งยิง :hover ให้ตอนแตะครั้งแรกก็จริง
แต่มันไม่แน่นอน ต่างกันไปตามเบราว์เซอร์ และทำให้ต้องแตะสองครั้งกว่าจะกดลิงก์ได้ ซึ่งแย่กว่าเดิม
ตัวเลือกที่ 4 ใช้เกณฑ์ผิด: ความกว้างจอไม่ได้แปลว่ามีหรือไม่มีเมาส์
แท็บเล็ตจอใหญ่ก็ไม่มี hover ส่วนหน้าต่างที่ย่อแคบบน desktop ก็ยังมีเมาส์
จุดที่ต้องจำ: :hover ใช้เน้นของที่เห็นอยู่แล้วได้
แต่ใช้เปิดเผยของที่ยังไม่เห็นไม่ได้ — ถ้าเนื้อหาหายแล้วผู้ใช้ทำงานต่อไม่ได้ คือใช้ผิดที่
2. เขียน button:focus { outline: none; } ไว้บรรทัดเดียวโดยไม่เขียนอะไรเพิ่ม ผลที่ตามมาคืออะไร?
:focus เป็นจริงทุกกรณีที่ element ได้โฟกัส ไม่ว่าจะมาจากเมาส์หรือคีย์บอร์ด
การลบ outline ที่นั่นจึงลบทิ้งหมดทุกทาง
ตัวเลือกที่ 1 คือสิ่งที่หลายคนเข้าใจว่าจะเกิด — แต่เบราว์เซอร์ไม่ได้ใส่กรอบกลับมาให้เอง
ถ้าอยากได้ผลแบบนั้นต้องเขียน :focus-visible เพิ่มเองเสมอ
ตัวเลือกที่ 3 สับสนระหว่าง "มองไม่เห็นโฟกัส" กับ "โฟกัสไม่ได้" — ปุ่มยังโฟกัสได้ปกติ
และยังกด Enter สั่งงานได้ มันแค่ไม่มีอะไรให้ตาเห็น ซึ่งอันตรายกว่าเพราะดูเหมือนไม่มีอะไรเสีย
ตัวเลือกที่ 4 ผิดเรื่อง specificity — สไตล์ default ของเบราว์เซอร์อยู่ชั้นล่างสุดเสมอ
กฎที่เราเขียนเองชนะทุกครั้ง ไม่ว่าจะเขียนสั้นแค่ไหน
จุดที่ต้องจำ: outline: none ไม่ผิดในตัวมันเอง —
ที่ผิดคือลบแล้วไม่วาดอะไรกลับมา ลบเมื่อไหร่ ต้องมี :focus-visible ตามมาเสมอ
3. อยากทำ <div class="my-button"> ให้กดไม่ได้ โดยเขียน .my-button:disabled { opacity: 0.5 } — จะเกิดอะไรขึ้น?
ผมทดสอบในเบราว์เซอร์ตรง ๆ ว่า <div> ตรงกับ :enabled ไหม ได้ false
และ <a href="#"> ก็ได้ false เหมือนกัน
ส่วน <input> ได้ true — สอง pseudo-class นี้ใช้กับ form control เท่านั้น
แปลว่ากฎนั้นไม่มีวันถูกใช้ ไม่ว่าคุณจะแปะ attribute disabled ให้ <div> หรือไม่ก็ตาม
ตัวเลือกที่ 1 ผิดทั้งสองท่อน: <div> ไม่จางลง (เพราะกฎไม่ทำงาน)
และ CSS ไม่เคยทำให้อะไร "กดไม่ได้" อยู่แล้ว — สิ่งที่ปิดการกดจริง ๆ คือ attribute ใน HTML
ตัวเลือกที่ 2 ถูกครึ่งเดียวและเป็นตัวที่หลอกที่สุด: pointer-events: none
ปิดการคลิกได้จริง แต่ <div> จะไม่จางลง เพราะกฎแรกไม่ทำงานตั้งแต่ต้น
และถึงปิดคลิกได้ก็ยังไม่แก้ปัญหาคีย์บอร์ด
ตัวเลือกที่ 3 ผิดเรื่องพฤติกรรมของ CSS — selector ที่ไม่ตรงกับอะไรถูกข้ามไปเงียบ ๆ
ไม่เคยทำให้ไฟล์พัง นี่แหละคือสิ่งที่ทำให้ bug ประเภทนี้หายากมาก
จุดที่ต้องจำ: อยากได้พฤติกรรมของปุ่ม ให้ใช้ <button>
แล้วคุณจะได้ :disabled · การข้าม Tab · และการปิด click มาฟรีทั้งชุด —
ทำเองด้วย <div> ต้องไล่เขียนเองทุกอย่าง แล้วก็จะลืมบางอย่างเสมอ
4. เขียน CSS ของปุ่มไว้ตามลำดับนี้: :active · :hover · :focus-visible — กดค้างด้วยเมาส์แล้วสีของ :active ไม่โผล่ ทำไม?
ตอนกดค้าง เงื่อนไขเป็นจริงพร้อมกันสองอย่าง (เมาส์อยู่เหนือปุ่ม + กำลังกด)
ทั้ง .btn:active และ .btn:hover นับ specificity ได้เท่ากันเป๊ะ
(class 1 + pseudo-class 1) → CSS ตัดสินด้วยลำดับที่เขียน ตัวล่างชนะ
ตัวเลือกที่ 2 ผิดสองชั้น: specificity ไม่ได้ต่างกัน และ !important
คือการเอาค้อนทุบปัญหาลำดับ ซึ่งจะย้อนกลับมาทำให้แก้อะไรไม่ได้ในไฟล์ใหญ่ ๆ — แค่สลับบรรทัดก็จบ
ตัวเลือกที่ 3 ผิดที่ข้อเท็จจริง :active ใช้ได้กับ element อะไรก็ได้ รวมทั้ง <div>
ตัวเลือกที่ 4 ผิดตรงที่ การคลิกด้วยเมาส์ไม่ทำให้ :focus-visible เป็นจริง
บนปุ่ม — นั่นคือเหตุผลทั้งหมดที่ pseudo-class ตัวนี้ถูกสร้างขึ้นมา
จุดที่ต้องจำ: เจอ CSS ที่ "เขียนไปแล้วไม่เห็นผล" ให้เช็คสองอย่างตามลำดับ —
specificity เท่ากันไหม แล้วค่อยดู ใครเขียนทีหลัง
เกือบทุกครั้งคำตอบอยู่ที่ข้อสอง และแก้ได้ด้วยการย้ายบรรทัด ไม่ใช่เพิ่มโค้ด
อ้างอิง
- MDN —
:focus-visible— อ่านหัวข้อ focus-visible vs focus ย่อหน้าเดียว แล้วเลื่อนลงไปดูตัวอย่างที่เขาเทียบปุ่มกับ<input>ให้ดู จะเห็นว่าทำไมช่องกรอกข้อความได้กรอบแม้คลิกด้วยเมาส์ แต่ปุ่มไม่ได้ - WCAG 2.1 — ข้อ 2.4.7 Focus Visible — อ่านแค่ประโยคเดียวของข้อนี้ (ยาวสองบรรทัด) แล้วคุณจะมีข้ออ้างที่เป็นมาตรฐานสากล ไว้ตอบทีม design ที่ขอให้ลบกรอบ focus ทิ้ง
- MDN — media feature
hover— ดูตารางค่าที่รับได้ (none/hover) และอ่านหมายเหตุเรื่องอุปกรณ์ที่มีทั้งจอสัมผัสและเมาส์ ซึ่งเป็นเคสที่ทำให้หลายคนพลาด - MDN —
:disabled— อ่านบรรทัดที่ระบุว่าใช้กับ element ชนิดไหนได้บ้าง จะเห็นว่ามีแต่ form control กับ<fieldset>· ไม่มี<div>และไม่มี<a> - Material Design 3 — ไปที่ Foundations → Interaction states ซึ่งเป็นที่มาโดยตรงของรายการ state ในสไลด์หน้า 58–64 (enabled · hover · focused · pressed · dragged · disabled) ใช้เทียบดูว่าสไลด์ตัดอะไรออกไปบ้าง
- CSS-Tricks Guides — ไม่ต้องอ่านทั้งหน้า ให้บุ๊กมาร์กไว้แล้วเปิดตอนติดเรื่อง property ตัวใดตัวหนึ่ง เป็นแหล่งที่อธิบายพร้อมภาพประกอบดีที่สุดสำหรับมือใหม่