JavaScript เปลี่ยนเอกสารสถิต (static documents) ให้กลายเป็นซอฟต์แวร์ แอปพลิเคชันหน้าเดียว (Single-page apps) ให้ความรู้สึกที่รวดเร็วทันใจ ไม่ต้องโหลดหน้าใหม่ทั้งหมด ไม่มีการกะพริบของหน้าจอสีขาว แต่ความเร็วนี้ต้องแลกมาด้วยราคาที่หลายทีมมองข้าม นั่นคือกลไกพื้นฐานของเว็บเริ่มเสื่อมสภาพ การนำทาง (Navigation) เริ่มเปราะบาง Search engine เริ่มติดตามเส้นทางได้ยากขึ้น Screen reader ก็หลงทาง และผู้ใช้พบว่าตัวเองติดอยู่ในอินเทอร์เฟซที่ดูเหมือนเว็บไซต์ แต่ทำงานเหมือนแอปพลิเคชันบนเดสก์ท็อปที่พังๆ
ตัวการมักจะเป็น div ที่มี onClick handler
เลิกใช้ Div เป็นลิงก์
div ไม่มีนัยสำคัญทาง semantic มันเป็นแค่กล่อง เมื่อคุณเอา click handler ไปผูกไว้กับมันแล้วใช้มันเพื่อนำทางผู้ใช้ไปยังมุมมองใหม่ คุณกำลังขอให้เบราว์เซอร์ปฏิบัติกับกล่องกระดาษเหมือนกับประตู ซึ่งเบราว์เซอร์จะปฏิเสธ รวมถึงเครื่องมือทุกอย่างที่สร้างขึ้นรอบๆ เบราว์เซอร์ด้วย
Screen reader จะไม่ประกาศว่า div คือลิงก์หรือปุ่ม พวกมันจะข้ามไป หรืออ่านมันเป็นแค่ข้อความธรรมดา ผู้ใช้ที่นำทางด้วยเสียงไม่สามารถระบุเป้าหมายได้ ส่วน crawler ของ search engine ที่กำลังสแกนหน้าเว็บของคุณเพื่อหา URL ที่ค้นพบได้ ก็จะไม่เห็นอะไรให้ติดตามเลย เส้นทาง (route) ของคุณอาจจะเหมือนกับไม่มีอยู่จริง
ที่แย่กว่านั้นคือ คุณจะสูญเสียพฤติกรรมที่ผู้ใช้คุ้นเคยไป ลิงก์จริงๆ ช่วยให้คนสามารถคลิกขวาเพื่อเปิดในแท็บใหม่, บันทึก (bookmark) ปลายทาง หรือคัดลอกที่อยู่เพื่อแชร์ได้ ผู้ใช้ที่ใช้คีย์บอร์ดคาดหวังว่าจะกด Tab เพื่อไปยังจุดนั้น และกด Enter เพื่อเปิดมัน แต่ div ไม่มีสิ่งเหล่านี้เลย แม้ว่าคุณจะพยายามเสริม tabIndex, role="link" และ keyboard listeners เข้าไป คุณก็กำลังสร้างสิ่งที่เบราว์เซอร์ให้คุณมาฟรีๆ ขึ้นมาใหม่แบบลวกๆ และคุณก็จะลืมกรณีพิเศษ (edge case) อยู่ดี คุณลืมมันเสมอแหละ
ใช้ Anchors สำหรับปลายทาง และ Buttons สำหรับการกระทำ
HTML แก้ปัญหานี้ไว้เรียบร้อยแล้ว ความสับสนเกิดขึ้นเพราะทั้งสององค์ประกอบดูเหมือนจะคลิกได้ นักพัฒนาจึงปฏิบัติกับพวกมันเหมือนว่าใช้แทนกันได้ แต่มันไม่ใช่
ใช้แท็ก <a เมื่อคุณต้องการย้ายผู้ใช้ไปยัง URL ใหม่ ไม่ใช่การจำลองการสลับมุมมอง (view swap) หรือการเปลี่ยนสถานะ (state change) แต่เป็นตำแหน่งที่อยู่จริง โดยแอตทริบิวต์ href ควรมีที่อยู่จริง:
<a href="/docs">Documentation</a>
แค่นั้นเอง ถ้าผู้ใช้กำลังจะไปที่ไหนสักแห่ง ให้ใช้ลิงก์
ใช้ <button> เมื่อมีบางอย่างเกิดขึ้นในหน้าปัจจุบัน ปุ่มมีไว้สำหรับการกระทำ เช่น:
- การเปิด modal
- การส่งฟอร์ม (submitting a form)
- การบันทึกการตั้งค่า
- การสลับเมนู (toggling a menu)
ลิงก์มีไว้สำหรับปลายทาง ปุ่มมีไว้สำหรับการกระทำ การผสมทั้งสองอย่างเข้าด้วยกันจะทำให้หน้าตาอินเทอร์เฟซของคุณสับสนและทำลายความคาดหวังของผู้ใช้
ปล่อยให้เบราว์เซอร์ทำหน้าที่ของมัน
เบราว์เซอร์สมัยใหม่เป็นผลมาจากการวิวัฒนาการและการกำหนดมาตรฐานมานานหลายทศวรรษ พวกมันจัดการเรื่องความปลอดภัย, ประวัติการเข้าชม (history), การดึงข้อมูลล่วงหน้า (prefetching) และการเข้าถึง (accessibility) ได้ดีกว่า JavaScript ที่คุณเขียนขึ้นมาเองอย่างแน่นอน
แท็ก anchor ที่แท้จริงจะป้อนข้อมูลเข้าสู่ browser history stack โดยอัตโนมัติ มันทำงานร่วมกับ context menu พื้นฐาน มันมีส่วนร่วมในอัลกอริทึม prefetching ที่ติดตั้งมาในตัวของเบราว์เซอร์เมื่อผู้ใช้เอาเมาส์ไปวาง (hover) หรือโฟกัสที่มัน ทำให้แอปของคุณรู้สึกเร็วขึ้นโดยที่คุณไม่ต้องเขียนโค้ดแม้แต่บรรทัดเดียว มันเคารพการตั้งค่าของผู้ใช้ในการเปิดลิงก์ และทำงานร่วมกับโปรแกรมจัดการรหัสผ่าน, เครื่องมือแปลภาษา และโหมดการอ่าน (reader modes)
เมื่อคุณแทนที่สิ่งนั้นด้วยฟังก์ชันการนำทางของ JavaScript คุณกำลังปฏิเสธสิ่งเหล่านั้นทั้งหมด คุณไม่ได้แค่สูญเสียฟีเจอร์เท่านั้น แต่คุณกำลังบังคับให้ผู้ใช้ต้องละทิ้งนิสัยที่พวกเขาสร้างขึ้นจากเว็บไซต์อื่นๆ ทั้งหมดบนอินเทอร์เน็ต นั่นไม่ใช่การตัดสินใจทางเทคนิค แต่มันคือประสบการณ์ผู้ใช้ที่แย่ (hostile user experience)
ตรวจสอบว่า Framework ของคุณเรนเดอร์อะไรออกมาจริงๆ
React Router, Vue Router, Next.js Link components, SvelteKit เครื่องมือเหล่านี้ทำให้การทำ client-side routing ดูเหมือนเป็นเรื่องง่าย แต่การใช้ abstraction ก็สร้างความผิดพลาดได้เช่นกัน
ตรวจสอบ DOM ของคุณ เปิด developer tools ของเบราว์เซอร์และดูองค์ประกอบที่ framework ของคุณส่งออกมา คอมโพเนนต์ <Link> ควรจะเรนเดอร์ออกมาเป็นแท็ก <a> จริงๆ พร้อมกับแอตทริบิวต์ href ที่ถูกต้องใน HTML สุดท้าย หากมันเรนเดอร์ออกมาเป็น <span>, <div> หรืออย่างอื่นที่ไม่มี href ที่เหมาะสม แสดงว่า abstraction ของคุณล้มเหลวแล้ว จงแก้ไขคอมโพเนนต์นั้น เขียนทับค่าเริ่มต้น (override the default) ใช้ prop passHref หรือสิ่งที่เทียบเท่าใน framework นั้นๆ อย่าเชื่อใจ framework ว่าจะทำออกมาได้ถูกต้องโดยไม่มีการตรวจสอบ
เรื่องนี้ยังสำคัญต่อปัญหา hydration mismatches ด้วย หากเซิร์ฟเวอร์เรนเดอร์ลิงก์ออกมา แต่ฝั่งไคลเอนต์ทำ hydration ให้กลายเป็นสิ่งที่ไม่ใช่ลิงก์ คุณกำลังสร้างบั๊กด้านการเข้าถึง (accessibility bugs) ที่ติดตามได้ยาก เพราะ HTML ดูเหมือนจะถูกต้องในซอร์สโค้ดของคุณ แต่ผิดพลาดใน DOM ที่ใช้งานจริง
แบนปลายทางปลอม
มีรูปแบบหนึ่งที่ไม่ยอมตายไปเสียที นั่นคือ href="javascript:void(0)" นักพัฒนามักใช้มันเมื่อต้องการบางอย่างที่ดูเหมือนลิงก์แต่ทำงานเหมือนปุ่ม โดยปกติเป็นเพราะพวกเขาไม่อยากจัดสไตล์ให้กับปุ่ม หรือเพราะโค้ดเก่า (older codebase) บังคับให้ทำแบบนั้น
Stop. This is not a URL. It gives the browser no destination. It pollutes the history stack with unusable states. It breaks browser history and accessibility. It is a trap. If you need click behavior without navigation, you need a <button>. Style it to look however you want. CSS does not care whether the element is a button or a link. Your users do.
Write Text That Explains Where the User Is Going
The words inside your link matter. Screen reader users often pull up a list of every link on the page to scan quickly. If your links all say "Read more" or "Click here," that list becomes useless noise.
Be specific. Compare these:
- Bad:
<a href="/security/api-guide">Read more</a> - Good:
<a href="/security/api-guide">Read the API security guide</a>
The second tells the user exactly what they will find. It gives search engines context about the destination page. It makes your link list navigable. Descriptive link text is one of the cheapest accessibility wins you can earn.
Test Like You Mean It
Architecture means nothing if you do not verify it.
First, test your keyboard flow. Unplug your mouse. Tab through every interactive element on your site. Every genuine link must show a visible focus outline, not a subtle glow that disappears against your background, but a clear ring that a tired eye can spot. Press Enter. It must activate the link. If Tab skips an element, or if Enter does nothing, you have a bug.
Second, test your routes at the server level. Client-side routing is a thin veneer. If a user bookmarks /dashboard/reports and returns tomorrow, or hits refresh, your server must know how to serve that page. Configure your reverse proxy or your server framework to fall back to your application shell for unknown paths, or serve the correct HTML directly. A dead 404 on refresh is not a minor bug. It is a broken promise.
JavaScript is a powerful layer
