ภาษาไทยบนเว็บ ตัดคำ ฟอนต์ เวลาอ่าน อะไรพัง และแก้ยังไงใน 5 เว็บ
เบราว์เซอร์ตัดคำไทยด้วยพจนานุกรม พลาดคำทับศัพท์ในหัวข้อใหญ่ keep-all ไม่ช่วย วิธีแก้ที่ใช้จริง เวลาอ่านนับตัวอักษร คู่ฟอนต์ไทย+อังกฤษ line-height และ lang
- เบราว์เซอร์หาจุดตัดคำไทยจากพจนานุกรม คำทับศัพท์อย่าง คลาวด์ โปรเจกต์ ไม่อยู่ในนั้น หัวข้อใหญ่จึงหักกลางคำ word-break keep-all ไม่ช่วย เพราะทำมาเพื่อ CJK
- วิธีแก้ที่ได้ผลคือห่อวลีด้วย span ที่ nowrap และเว้นวรรครอบคำทับศัพท์ในหัวข้อ ให้ตัดได้แค่ระหว่างวลี
- เวลาอ่านของไทยต้องนับตัวอักษร (เราใช้ 900 ตัว/นาที) นับคำแบบอังกฤษ (220 คำ/นาที) จะได้เลขผิดหลายเท่า
- ไทยมีสระและวรรณยุกต์ซ้อนบนล่าง ต้องให้ line-height มากกว่าอังกฤษ และฟอนต์ไทยกับอังกฤษควรเป็นตระกูลเดียวกันหรือ x-height ใกล้กัน
เว็บภาษาไทยที่ดูดีบนจอคอมพิวเตอร์ เปิดบนมือถือแล้วหัวข้อหักคำว่า “คลาวด์” เป็น “คลา” กับ “วด์” คนละบรรทัด เวลาอ่านบอก 2 นาที ทั้งที่อ่านจริง 6 นาที และฟอนต์ไทยกับอังกฤษในประโยคเดียวกันสูงไม่เท่ากัน สามเรื่องนี้เราเจอครบตอนทำเว็บนี้ แล้วเจออีกครั้งตอนทำอีกสี่เว็บด้วย เอนจินเดียวกัน บทความนี้รวมสิ่งที่พัง เหตุผล และโค้ดที่ใช้แก้จริง
ตรวจกับ Chrome, Safari และ Firefox รุ่นปัจจุบัน (ต.ค. 2026) บน Windows 11, macOS และ iPhone ฟอนต์จาก Google Fonts
ไทยไม่มีช่องว่าง เบราว์เซอร์ต้องเดาคำ
ภาษาอังกฤษตัดบรรทัดที่ช่องว่าง ภาษาไทยไม่มีช่องว่างระหว่างคำ ช่องว่างที่มีคั่น “วลี” ไม่ใช่ “คำ” เบราว์เซอร์จึงต้องหาเองว่าคำจบตรงไหน สเปก CSS Text เขียนไว้ตรง ๆ ว่าภาษาไทย ลาว เขมร ต้องใช้ “lexical resource” คือพจนานุกรม Chrome และ Safari ใช้ตัวตัดคำแบบพจนานุกรมของ ICU ที่มีรายการคำไทยติดมา แต่ละเอนจินถือข้อมูลคนละชุด ผลจึงต่างกันได้บ้าง
พจนานุกรมพวกนี้รู้จักคำไทยทั่วไปดี ปัญหาอยู่ที่คำทับศัพท์ “คลาวด์” “โปรเจกต์” “เซิร์ฟเวอร์” พอไม่รู้จักทั้งคำ ตัวตัดคำก็หาคำย่อยที่รู้จักข้างใน แล้วตัดตรงนั้น ในย่อหน้าตัวหนังสือ 17px โอกาสที่จุดตัดจะตกกลางคำทับศัพท์มีน้อย แต่ h1 ขนาด 46px บนจอ 375px มีที่ให้แค่ 7-8 ตัวอักษรต่อบรรทัด ทุกบรรทัดต้องตัดสักที่ และที่นั้นมักเป็นกลางคำทับศัพท์
keep-all ไม่ช่วย
สิ่งแรกที่คนลองคือ word-break: keep-all พูดให้ชัด มันไม่ได้ผลกับภาษาไทย MDN เขียนว่า keep-all คือ “ห้ามตัดคำในข้อความจีน ญี่ปุ่น เกาหลี (CJK) ส่วนข้อความอื่นทำตัวเหมือน normal” และสเปก CSS Text ระบุว่าใน keep-all การตัดยังเกิดได้ “ตรงที่มีโอกาสตัดจาก dictionary-based breaking” ซึ่งก็คือภาษาไทยพอดี ทุกค่าของ word-break และ line-break ยังตัดไทยตามพจนานุกรมเดิม จุดที่ผิดก็ยังผิดเหมือนเดิม
เบราว์เซอร์ตัดคำไทยด้วยพจนานุกรม ไม่มี property ของ CSS ตัวไหนสอนคำใหม่ให้มัน ทางแก้คือบอกว่าตรงไหนห้ามตัด
วิธีแก้ 1: ห่อวลีด้วย nowrap
หัวข้อหน้าแรกเขียนด้วยมือ เราห่อแต่ละวลีด้วย <span class="nw"> ที่เป็น white-space: nowrap บรรทัดจะตัดได้เฉพาะระหว่าง span
<!-- ก่อน: เบราว์เซอร์ตัดได้ทุกจุดที่พจนานุกรมบอก -->
<h1>สร้างระบบที่ทำงานได้จริง บนคลาวด์และหน้างาน</h1>
<!-- หลัง: ตัดได้แค่ระหว่างวลี คลาวด์ ไม่มีทางหัก -->
<h1><span class="nw">สร้างระบบที่ทำงานได้จริง</span> <span class="nw">บนคลาวด์และหน้างาน</span></h1>
.nw{white-space:nowrap}
วลีต้องสั้นพอให้อยู่ในจอ 375px ที่ขนาดฟอนต์นั้น 14 ตัวอักษรที่ 36px กว้างราว 300px พอดี ยาวกว่านั้นจะล้นออกขวาเพราะห้ามตัด
วิธีแก้ 2: เว้นวรรครอบคำทับศัพท์ในหัวข้อบทความ
หัวข้อบทความมาจาก frontmatter ใส่ span ไม่ได้ เราเขียน component เล็ก ๆ ThaiTitle.astro แยกหัวข้อตามช่องว่าง แล้วห่อแต่ละชิ้นที่ยาวไม่เกิน 14 ตัวอักษรด้วย .nw ชิ้นที่ยาวกว่าปล่อยให้ตัดปกติเพื่อไม่ให้ล้นจอ ภาษาอังกฤษผ่านไปโดยไม่แตะ
const parts = text.split(' ');
{lang === 'th'
? parts.map((w, i) => <>{i > 0 && ' '}{w.length <= 14 ? <span class="nw">{w}</span> : w}</>)
: text}
เวลาเขียนหัวข้อไทย ถ้าคำทับศัพท์ไหนหักบ่อย แค่เว้นวรรครอบคำนั้น
# ก่อน: โปรเจกต์ อาจหักเป็น โปร / เจกต์
title: "ทำไมโปรเจกต์ต้องมี README ตั้งแต่วันแรก"
# หลัง: โปรเจกต์ เป็นชิ้นของตัวเอง ThaiTitle ห่อให้ทั้งคำ
title: "ทำไม โปรเจกต์ ต้องมี README ตั้งแต่วันแรก"
ช่องว่างเพิ่มหนึ่งตัวในหัวข้อไทยอ่านไม่สะดุด เพราะไทยใช้ช่องว่างคั่นวลีอยู่แล้ว และเป็นการแก้ที่ข้อมูล ไม่ใช่ CSS
ผลข้างเคียงฝั่งอังกฤษ และกฎ 520px
.nw ตัวเดียวกันถูกใช้กับ hero ภาษาอังกฤษด้วย เว็บที่ใช้ฟอนต์หัวข้อตัวกว้าง (Kanit) วลีอังกฤษวลีเดียวยาวกว่าจอมือถือแล้วล้นออกขวา ไทยไม่เป็นเพราะวลีสั้นกว่า อังกฤษมีช่องว่างและเบราว์เซอร์ตัดไม่ผิด จึงปล่อยให้อังกฤษตัดปกติบนจอแคบ
/* English has spaces to break at, and a wide display face can make one phrase wider than a phone */
@media (max-width:520px){html[lang="en"] .nw{white-space:normal}}
กฎนี้อยู่ใน CSS ร่วมของเอนจิน แก้ครั้งเดียวได้ทั้งสี่เว็บ
เวลาอ่าน นับตัวอักษร ไม่นับคำ
สูตรทั่วไปคือนับคำหารด้วย 200-250 คำต่อนาที ในโค้ด “นับคำ” คือ split(/\s+/) ซึ่งนับช่องว่าง บทความไทย 5,500 ตัวอักษรอาจมีช่องว่างแค่ 400 ตัว สูตรนี้จึงตอบ 2 นาที ทั้งที่อ่านจริงราว 6 นาที
เราแยกสองสูตรตามภาษาของไฟล์ ไทยนับตัวอักษร 900 ตัวต่อนาที อังกฤษนับคำ 220 คำต่อนาที โค้ดนับสองเท่าทั้งคู่ และตัด SVG ของแผนภาพออกก่อน
const m = postLang(p) === 'th'
? (prose.length + codeBlocks.length * 2) / 900
: (prose.split(/\s+/).length + codeBlocks.split(/\s+/).length * 2) / 220;
return Math.max(1, Math.round(m));
ตัวเลข 900 มาจากการจับเวลาอ่านบทความของเราเองสองสามบท ไม่ใช่ค่าวิจัย ถ้าคนอ่านเป็นผู้เริ่มต้น 700 ก็สมเหตุสมผล สิ่งสำคัญคือหน่วยต้องเป็นตัวอักษร
คู่ฟอนต์ไทย + อังกฤษ
ฟอนต์ไทยแบ่งหยาบ ๆ เป็น “มีหัว” (looped) หัวกลมเล็กที่ต้นเส้นแบบหนังสือเรียน อ่านยาวสบายและดูเป็นทางการ กับ “ไม่มีหัว” (loopless) ที่ตัดหัวออกให้รูปทรงใกล้อักษรละติน ดูทันสมัย แต่บางคู่ (ด กับ ค) สับสนได้ที่ขนาดเล็ก ห้าเว็บของเราเลือกตามน้ำเสียงของเว็บ
| เว็บ | หัวข้อ (display) | เนื้อหา (body) | ป้าย/โค้ด (mono) | เหตุผล |
|---|---|---|---|---|
| 6amdev | Bai Jamjuree 600-700 | IBM Plex Sans Thai 400-600 | IBM Plex Mono | เว็บช่าง ไม่มีหัว คม |
| devmakerx | Chakra Petch 600-700 | IBM Plex Sans Thai | JetBrains Mono | โทนเมกเกอร์ ตัวเหลี่ยม |
| mykaset | Kanit 500-600 | IBM Plex Sans Thai | IBM Plex Mono | เกษตร เป็นมิตร ตัวหนาแน่น |
| aroysure | Mitr 400-700 | IBM Plex Sans Thai Looped | Mitr | เว็บอาหาร ป้ายใช้ Mitr แทนฟอนต์โค้ด อ่านอบอุ่นกว่า |
| pickoshop | Pridi 500-600 | IBM Plex Sans Thai Looped | ใช้ body แทน | ร้านค้า มีหัว นุ่ม ไม่มีโค้ดบนเว็บ |
- ตระกูลเดียวกันก่อน IBM Plex Sans Thai กับ IBM Plex Mono มี x-height เดียวกัน ตัวเลขและคำอังกฤษในประโยคไทยจึงไม่ลอยไม่จม ถ้าตระกูลไม่ตรง ดูคำว่า “API” กลางประโยคไทยที่ 16px
- ฟอนต์หัวข้อที่มีละตินในตัว Bai Jamjuree, Kanit, Mitr, Chakra Petch, Pridi มีอักษรละตินมาด้วย หัวข้อผสมไทย-อังกฤษจึงเป็นฟอนต์เดียว ไม่มีฟอนต์สำรองมาปน
- น้ำหนักต่างกันตามฟอนต์ เอนจินตั้งหัวข้อ 700 แต่ Mitr และ Kanit ที่ 700 ตันเกินไปสำหรับไทย comment ใน tokens.css ของ aroysure เขียนไว้ว่า “Mitr reads best a step lighter” จึง override เป็น 600
- โหลดเฉพาะชุดที่ใช้ Google Fonts ส่ง
@font-faceแยกตามชุดอักษร ชุด thai ครอบU+0E01-0E5Bและเครื่องหมายไม่กี่ตัว เบราว์เซอร์โหลดเฉพาะเมื่อหน้านั้นมีตัวอักษรในช่วงนั้น เราขอแค่น้ำหนักที่ใช้ (Bai+Jamjuree:wght@600;700,IBM+Plex+Sans+Thai:wght@400;500;600) และใส่display=swap
line-height ไทยต้องสูงกว่า
ตัวอักษรไทยซ้อนได้สามชั้น พยัญชนะ สระบน วรรณยุกต์ (เช่น “ที่” “ปั้น”) และยังมีสระล่าง (อุ อู) line-height 1.5 ที่พออังกฤษ ทำให้วรรณยุกต์บรรทัดล่างชนสระล่างของบรรทัดบน เราใช้ 1.85 กับเนื้อหาบทความ 17px, 1.7 กับข้อความทั่วไป และ 1.2 กับหัวข้อ ที่ตัวใหญ่มีช่องว่างสัมพัทธ์พออยู่แล้ว ต่ำกว่านี้วรรณยุกต์ถูกตัด หัวข้อยังใส่ text-wrap: balance ซึ่งอยู่ร่วมกับ .nw ได้ เพราะ balance แค่เลือกจากจุดตัดที่อนุญาตอยู่แล้ว
lang ทำอะไร
<html lang="th"> และ lang="en" ที่หน้าอังกฤษ ไม่ได้ทำให้ตัดคำไทยแม่นขึ้น เบราว์เซอร์ตัดไทยตามตัวอักษร แต่มันทำสามอย่างที่ต้องการ ระบบเลือกฟอนต์สำรองที่ถูกก่อนฟอนต์เว็บมา (iOS หยิบ Thonburi ให้หน้าไทย ไม่ใช่ฟอนต์จีน) hyphens: auto ของหน้าอังกฤษทำงานได้ (ต้องมี lang เสมอ) และ screen reader อ่านด้วยเสียงที่ถูก ปุ่มสลับภาษาจึงใส่ lang ของภาษาปลายทางด้วย และมันคือสิ่งที่ html[lang="en"] .nw ข้างบนใช้แยกกฎ
ข้อผิดพลาดที่เจอบ่อย
- ใส่
word-break: keep-allหรือline-break: strictแล้วคิดว่าแก้ไทยได้ ทั้งสองค่าทำเพื่อ CJK - ห่อทั้งหัวข้อด้วย nowrap อันเดียว บนจอ 375px ล้นออกขวาทันที
- โหลดฟอนต์ไทยทุกน้ำหนัก 100-900 ทั้งที่ใช้สามน้ำหนัก
- ใช้ line-height 1.5 ตามแม่แบบอังกฤษ แล้ววรรณยุกต์ชนบรรทัดบน
- นับเวลาอ่านด้วย
split(/\s+/)กับไฟล์ไทย ได้เลขน้อยกว่าจริงราว 3 เท่า
แหล่งอ้างอิง
- CSS Text Module Level 3, Line Breaking Details และ word-break ไทยต้องใช้ lexical resource และ keep-all ยกเว้นการตัดจากพจนานุกรม
- MDN: word-break นิยาม keep-all สำหรับ CJK
- W3C i18n: Approaches to line breaking ไทย ลาว เขมร ใช้ช่องว่างคั่นวลี เบราว์เซอร์ใช้พจนานุกรม
- Google Fonts CSS API การขอน้ำหนัก display=swap และ unicode-range ต่อชุดอักษร
คำถามที่พบบ่อย
ทำไมไม่ใส่ zero-width space (U+200B) ระหว่างคำไทยทุกคำไปเลย
ทำได้ แต่ต้องมีตัวตัดคำตอน build ตัวอักษรที่มองไม่เห็นจะหลุดไปใน copy-paste และผลค้นหา และเสียเวลากับทุกบทความ สำหรับเรา ปัญหาเกิดเฉพาะหัวข้อตัวใหญ่ที่คำน้อย การเว้นวรรครอบคำทับศัพท์ในหัวข้อจึงพอ
ใช้ฟอนต์ไทยจาก Google Fonts ช้าไหม
ไฟล์แยกตามชุดอักษร เบราว์เซอร์โหลดเฉพาะชุดที่หน้านั้นใช้จริงผ่าน unicode-range หน้าภาษาไทยโหลดชุด thai กับ latin เท่านั้น เราขอแค่ 2-3 น้ำหนักต่อตระกูล และใส่ display=swap ให้ข้อความขึ้นก่อนฟอนต์มา
lang="th" ช่วยให้เบราว์เซอร์ตัดคำไทยถูกขึ้นไหม
ไม่โดยตรง การตัดคำไทยดูจากตัวอักษรไทยในข้อความ ไม่ได้ดู lang แต่ lang ยังจำเป็นสำหรับฟอนต์สำรองของระบบ hyphens ของอังกฤษ screen reader และเป็นตัวที่ CSS ของเราใช้แยกกฎไทยกับอังกฤษ
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ