การนําทางแบบ Scrollspy เป็นรูปแบบ UI ทั่วไปที่ไฮไลต์ลิงก์การนําทางที่สอดคล้องกับส่วนของหน้าเว็บที่อยู่ในวิวพอร์ตโดยอัตโนมัติ ซึ่งจะช่วยให้ผู้ใช้ทราบได้อย่างชัดเจนว่าตนเองกำลังอ่านส่วนใดของเอกสารที่มีความยาวขณะเลื่อนดู
รูปแบบนี้มักพบใน 2 สถานการณ์ต่อไปนี้
- เว็บไซต์ที่มีหน้าเดียว: เว็บไซต์ที่ลิงก์การนำทางหลักไปยังส่วนต่างๆ ของหน้าเดียวแทนที่จะเป็น URL แยกกัน
- เนื้อหาแบบยาว: สารบัญหรือปุ่มนำทางที่แถบด้านข้างจะติดตามความคืบหน้าของผู้ใช้ผ่านบทความ คู่มือ หรือเอกสารประกอบ
วิธีการนำไปใช้งาน
โดยปกติแล้ว รูปแบบ Scrollspy ต้องใช้ JavaScript และ IntersectionObserver API CSS สมัยใหม่มีวิธีแบบเนทีฟในการดำเนินการนี้โดยใช้พร็อพเพอร์ตี้ scroll-target-group และคลาสเทียม :target-current
- ตรวจสอบว่าลิงก์การนำทางใช้ตัวระบุ Fragment: การนำทางต้องประกอบด้วยลิงก์ตำแหน่งเฉพาะ (
<a>) ที่ชี้ไปยังส่วนต่างๆ ในหน้าเดียวกันโดยใช้ตัวระบุ Fragment (เช่น<a href="#section-1">) ที่ตรงกับidของส่วนเนื้อหา - เปิดใช้การจัดกลุ่มเป้าหมายการเลื่อน: ใช้พร็อพเพอร์ตี้
scroll-target-group: autoกับคอนเทนเนอร์ของลิงก์การนําทาง ซึ่งจะบอกให้เบราว์เซอร์ถือว่าลิงก์เหล่านี้เป็นเครื่องหมายที่ติดตามเป้าหมายที่เกี่ยวข้อง - จัดรูปแบบลิงก์ที่ใช้งานอยู่: ใช้คลาสเทียม
:target-currentในลิงก์การนำทางเพื่อใช้รูปแบบเมื่อส่วนเป้าหมายปรากฏในคอนเทนเนอร์การเลื่อน - สถานะเชิงความหมาย: ใช้สคริปต์เพื่ออัปเดต
aria-current="true"ในลิงก์ที่ใช้งานอยู่เมื่อการเลื่อนหยุดลง เพื่อให้แน่ใจว่ามีการสื่อสารสถานะไปยังเทคโนโลยีความช่วยเหลือพิเศษอย่างถูกต้อง
โค้ดตัวอย่าง
ตัวอย่างนี้ไฮไลต์ลิงก์ที่ใช้งานอยู่ในเมนูการนำทางแบบคงที่ขณะที่ผู้ใช้เลื่อนดูส่วนต่างๆ
/* MANDATORY: The container of markers must have scroll-target-group: auto */
nav {
/* Enables automatic tracking of fragment links */
scroll-target-group: auto;
}
/* MANDATORY: Use :target-current to style the link whose target is in view. */
nav a:target-current {
color: var(--accent, #007bff);
border-bottom-color: var(--accent, #007bff);
}
<nav>
<a href="#intro">Introduction</a>
<a href="#features">Features</a>
<a href="#pricing">Pricing</a>
</nav>
<main>
<!-- MANDATORY: Each section must have a unique ID matching a link fragment in the <nav>. -->
<section id="intro">
<h2>Introduction</h2>
<!-- content -->
</section>
<section id="features">
<h2>Features</h2>
<!-- content -->
</section>
<section id="pricing">
<h2>Pricing</h2>
<!-- content -->
</section>
</main>
<script type="module">
if (CSS.supports('scroll-target-group: auto')) {
const syncAriaCurrent = () => {
const currentLink = document.querySelector('nav a:target-current');
document.querySelectorAll('nav a').forEach(link => {
link.setAttribute('aria-current', link === currentLink ? 'true' : 'false');
});
};
// Update the aria on initial page load. Ensure the DOM content is loaded, either by using `<script type="module">` or an event listener for `DOMContentLoaded`.
syncAriaCurrent();
// Update the aria after scroll ends
document.addEventListener('scrollend', syncAriaCurrent);
}
</script>
แนวทางปฏิบัติแนะนำ
- ควรใช้
scroll-target-group: autoในคอนเทนเนอร์เฉพาะสำหรับลิงก์การนำทาง เพื่อให้เบราว์เซอร์ทราบว่าควรติดตามลิงก์ใด - ควรตรวจสอบว่าเป้าหมายมีแอตทริบิวต์
idที่ไม่ซ้ำกันซึ่งตรงกับแอตทริบิวต์hrefของลิงก์ - ควรเว้นระยะห่างในแนวตั้งให้เพียงพอสำหรับแต่ละส่วน:
scroll-target-groupจะระบุเป้าหมายที่มองเห็นได้ชัดเจนที่สุดเมื่อแต่ละส่วนมีขนาดใหญ่พอที่จะมองเห็นได้ครั้งละ 1 หรือ 2 ส่วนเท่านั้น - บังคับ: สถานะภาพต้องสอดคล้องกับ
aria-currentเพื่อให้เข้าถึงได้ แม้ว่า:target-currentจะจัดการด้านภาพ แต่โปรแกรมอ่านหน้าจอจะไม่แสดงข้อมูลนี้ - AVOIDการใช้สีเพียงอย่างเดียวเพื่อแสดงสถานะที่ใช้งานอยู่ โดยให้ใช้ภาพอื่นๆ ร่วมด้วย เช่น ความหนาของแบบอักษร การขีดเส้นใต้ หรือจุดบอกสถานะ หรือตรวจสอบว่ามีคอนทราสต์ที่เพียงพอระหว่างสีต่างๆ เพื่อให้ผู้ใช้ที่มีความบกพร่องทางการมองเห็นสีสามารถเข้าถึงความแตกต่างได้
กลยุทธ์สำรอง
สำหรับเบราว์เซอร์ที่ไม่รองรับ scroll-target-group คุณควรใช้ทางเลือกสำรองที่อิงตาม IntersectionObserver API
<script>
// Feature detection for scroll-target-group
const supportsScrollTargetGroup = CSS.supports('scroll-target-group: auto');
if (!supportsScrollTargetGroup) {
const navLinks = document.querySelectorAll('nav a');
const sections = document.querySelectorAll('main section');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute('id');
navLinks.forEach(link => {
const isActive = link.getAttribute('href') === `#${id}`;
// Use the escaped colon class for consistent styling
link.classList.toggle('\:target-current', isActive);
link.setAttribute('aria-current', isActive);
});
}
});
}, { rootMargin: '-50% 0px -50% 0px', threshold: 0 });
sections.forEach(section => observer.observe(section));
}
</script>
<style>
/* Fallback style if :target-current is not supported.
Use :is() to combine the native pseudo-class with a fallback class for older browsers. DO NOT use `nav a:target-current, nav a.\:target-current, as the unrecognized pseudoselector invalidates the entire rule in older browsers. */
nav a:is(:target-current, .\:target-current) {
color: var(--accent, #007bff);
border-bottom-color: var(--accent, #007bff);
font-weight: bold;
}
</style>