如何用纯JavaScript实现滚动时导航菜单高亮及子菜单展开折叠?
Hey there! Let’s walk through building that scroll-responsive sidebar navigation you’re after—where menu items highlight as you scroll, and submenus expand only when their parent is active (with everything collapsed otherwise). No third-party libraries required, just plain JavaScript. Here’s a step-by-step breakdown that’s tried and tested:
1. Set Up Your HTML Structure First
You’ll need a sidebar navigation that links to corresponding content sections on the page. Use data attributes to map menu items to their target sections, and nest submenus inside parent list items:
<!-- Sidebar Navigation --> <nav class="sidebar"> <ul class="nav-list"> <li class="nav-item"> <a href="#intro" class="nav-link" data-target="#intro">Introduction</a> </li> <li class="nav-item has-submenu"> <a href="#features" class="nav-link" data-target="#features">Features</a> <ul class="submenu"> <li class="submenu-item"> <a href="#feature1" class="submenu-link" data-target="#feature1">Feature 1</a> </li> <li class="submenu-item"> <a href="#feature2" class="submenu-link" data-target="#feature2">Feature 2</a> </li> </ul> </li> <li class="nav-item"> <a href="#conclusion" class="nav-link" data-target="#conclusion">Conclusion</a> </li> </ul> </nav> <!-- Page Content Sections --> <section id="intro" class="content-section">...</section> <section id="features" class="content-section">...</section> <section id="feature1" class="content-section">...</section> <section id="feature2" class="content-section">...</section> <section id="conclusion" class="content-section">...</section>
2. Add Basic CSS for Highlighting & Submenu Toggle
Style the active states and submenu visibility to make the behavior visible:
/* Default nav styles */ .nav-link, .submenu-link { display: block; padding: 8px 16px; text-decoration: none; color: #333; transition: color 0.2s ease; } /* Active state for links */ .nav-link.active, .submenu-link.active { color: #007bff; font-weight: bold; } /* Submenu default state (collapsed) */ .submenu { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; padding-left: 20px; } /* Submenu expanded state */ .submenu.show { max-height: 200px; /* Adjust based on your submenu size */ }
3. Core JavaScript Logic
Now let’s build the functionality in plain JS. We’ll break this into key functions:
3.1 Debounce Function (For Performance)
Scroll events fire constantly—use a debounce to limit how often our logic runs, avoiding jank:
function debounce(func, delay = 100) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }
3.2 Detect Active Section
This function checks which content section is currently in the "active" viewport area (we’ll use the top 20% of the viewport as our trigger point for better UX, so the highlight feels natural):
function getActiveSection() { const sections = document.querySelectorAll('.content-section'); let activeSectionId = ''; const viewportHeight = window.innerHeight; const triggerPoint = viewportHeight * 0.2; // Trigger when section hits 20% from top sections.forEach(section => { const rect = section.getBoundingClientRect(); // Check if section is above the trigger point and not fully scrolled past if (rect.top <= triggerPoint && rect.bottom > triggerPoint) { activeSectionId = `#${section.id}`; } }); return activeSectionId; }
3.3 Update Navigation & Submenus
This function updates the active state of menu items and toggles submenus:
function updateNavigation() { const activeSectionId = getActiveSection(); if (!activeSectionId) return; // Remove active state from all links document.querySelectorAll('.nav-link, .submenu-link').forEach(link => { link.classList.remove('active'); }); // Collapse all submenus document.querySelectorAll('.submenu').forEach(submenu => { submenu.classList.remove('show'); }); // Find the active link const activeLink = document.querySelector(`[data-target="${activeSectionId}"]`); if (!activeLink) return; // Add active state to the link activeLink.classList.add('active'); // If the active link is in a submenu, expand its parent submenu and highlight parent const parentNavItem = activeLink.closest('.has-submenu'); if (parentNavItem) { const parentLink = parentNavItem.querySelector('.nav-link'); parentLink.classList.add('active'); parentNavItem.querySelector('.submenu').classList.add('show'); } // If the active link is a parent item, expand its submenu else if (activeLink.closest('.nav-item').classList.contains('has-submenu')) { activeLink.closest('.nav-item').querySelector('.submenu').classList.add('show'); } }
3.4 Initialize & Attach Scroll Listener
Run the update function on page load and attach the debounced scroll listener:
// Initialize on page load to set initial active state window.addEventListener('DOMContentLoaded', updateNavigation); // Attach debounced scroll listener to update on scroll window.addEventListener('scroll', debounce(updateNavigation)); // Optional: Update on viewport resize (also debounced) window.addEventListener('resize', debounce(updateNavigation));
4. Extra Tips for Polish
- Smooth Scrolling: Add
scroll-behavior: smooth;to your CSS<html>tag for nicer anchor link scrolling. - Bottom-of-Page Edge Case: Adjust the
getActiveSectionlogic to ensure the last section gets highlighted when scrolled to the very bottom of the page. - Accessibility: Add ARIA attributes like
aria-expandedto parent menu items to make the submenu behavior accessible to screen readers.
内容的提问来源于stack exchange,提问作者Himanshu sharma

