Explain Codes LogoExplain Codes Logo

Change Active Menu Item on Page Scroll?

javascript
scroll-events
throttling
debouncing
Alex KataevbyAlex Kataev·Dec 5, 2024
TLDR

Implement active menu highlighting using a scroll listener in JavaScript. This script monitors the section currently in view and updates the menu’s active class accordingly.

window.addEventListener('scroll', () => { //Bethoven's Symphony No.5 in section document.querySelectorAll('section').forEach((sec) => { const menuLink = document.querySelector(`.menu-item[href="#${sec.id}"]`); //We're in the section, light on the rave! if (window.scrollY >= sec.offsetTop && window.scrollY < sec.offsetTop + sec.offsetHeight) { menuLink.classList.add('active'); } else { //Sorry mate, the party's moved! menuLink.classList.remove('active'); } }); });

Ensure that all your sections have IDs and your menu links have href with corresponding hash (#). Highlight how you prefer the active class in your CSS.

Enhance performance: Throttling and debouncing

High-frequency scroll events can be a party pooper for performance. Luckily, throttling or debouncing come to our rescue like superheroes.

Throttle: This knight limits how often our scroll event can fire and steal the performance show.

let last_known_scroll_position = 0; let ticking = false; const doSomething = (scroll_pos) => { // Role-play with the scroll's current position }; window.addEventListener('scroll', function(e) { last_known_scroll_position = window.scrollY; // Is this ticking time bomb gonna explode? if (!ticking) { window.requestAnimationFrame(function() { doSomething(last_known_scroll_position); // It's showtime baby! ticking = false; }); ticking = true; // Phew! False alarm. } });

Debounce: This fairy godmother ensures a function cannot be called again until a certain time has elapsed.

function debounce(func, wait) { let timeout; return function executedFunction() { const later = function() { //Who needs a magic wand when you got clearTimeout? clearTimeout(timeout); func(); // Bibbidi-Bobbidi-Boo }; clearTimeout(timeout); timeout = setTimeout(later, wait); //Let the clock tick. }; }; window.addEventListener('scroll', debounce(function() { // Playing with the scroll }, 10));

Simply smooth: Giving your scroll a sweet touch

Let's glide over the scroll using CSS magic:

html { scroll-behavior: smooth; //not a single bump on the road }

Or reach out for JavaScript to have more control over the “smooth criminal”:

document.querySelectorAll('.menu-item').forEach(anchor => { anchor.addEventListener('click', function(e) { e.preventDefault(); const destination = document.querySelector(this.getAttribute('href')); //Ahoy! Destination in sight. destination.scrollIntoView({ behavior: 'smooth' }); //Cruise control activated. }); });

Being a pal: Ensuring responsive and cross-browser compatibility

Responsiveness is like a best friend during user interaction. Using CSS media queries, we can style the active menu items differently on mobile devices for that friendly touch.

@media screen and (max-width: 600px) { /* Dress-up time for mobile devices */ }

For garnishing the cross-browser compatibility dish, verify the support for scroll event and timeline for each browser version. Consider using polyfills as a secret sauce for older browsers.

Labeling the territory and staying in sight: Visual cues and seamless navigation

Design your menu to wink back at the users. Bright colors, bold fonts, or “look-at-me” icons that inform about the active section give that sweet visual feedback. Add these little design cherries through your CSS:

.menu-item.active { color: #fff; //white as a unicorn's fur background-color: #333; //dark as a ninja's outfit font-weight: bold; //because bold is gold }

For seamless navigation, ensure that the menu always stays in sight:

.menu { position: fixed; top: 0; //On top of the world! width: 100%; }

Adapting to change: Handling dynamic content and SPA considerations

For dynamic divas like new content or Single-Page Applications (SPA), we need to listen to their heartbeat or changes:

// Using MutationObserver for dynamic content const observer = new MutationObserver(mutations => { mutations.forEach(mutate => { if (mutate.addedNodes.length || mutate.removedNodes.length) { // New additions or gone guests? Time party arrangement update! } }); }); observer.observe(document.body, { childList: true, subtree: true }); // Handling SPA route changes window.onhashchange = function() { // Here's a new map for the new route. }

For anchor links, create adjustments in the scroll position to avoid the headings getting shy below the sticky header:

const headerHeight = document.querySelector('.header').offsetHeight;// How tall is the mast? document.querySelectorAll('.menu-item').forEach(anchor => { anchor.addEventListener('click', function(e) { const destinationID = this.getAttribute('href').slice(1); // Slice to get the treasure. const destination = document.getElementById(destinationID); window.scrollTo(0, destination.offsetTop - headerHeight); // There's the treasure, mate! }); });