Anchor link target hidden behind Divis’ sticky menu?

When you use Divi by Elegant Themes you most likely use the “sticky menu” feature, which is enabled by default. This makes the top menu sticky when you scroll further down the page.

However, there’s one problem that frequently happens on projects I work on: When targeting a Link-Anchor, e.g. by using an <a href="#anchor"></a> link on the current page, Divi scrolls down too far. In fact, the Link-Anchor is always hidden behind the sticky menu, when the page can be scrolled far enough.

There are a few examples on the web (like the one by Divi Booster) that work, but have some drawbacks:

  1. The script only fires once, during page load, but do not handle later clicks on anchor links that target the current site.
  2. When you want the page to scroll a little bit further down below the sticky menu, the code provides no solution. E.g. when you use a drop-shadow below the menu and want to keep additional 20px space.
  3. Practices are “workarounds” but no clean solutions. E.g. setting “display: none” to prevent scrolling or using global scope variables inside JS closures.

So, I wanted to create a modern solution that works well in 2019 and later. I started by adding an event handler for the hashchanged window event. So we do not depend on the DOM Loaded event (which only fires once per page) but can also intercept links that point to the current page.

window.addEventListener('DOMContentLoaded', function(ev) {
	if (window.location.hash) {
		// On initial page load set the hash to empty and apply
		// the hash again after a short delay.
		var origHash = window.location.hash;
		window.location.hash = '';

		$(function() {
			// This will trigger the "hashchanged" event below.
			window.setTimeout(function() {
				window.location.hash = origHash;
			}, 600);
		});
	}
});

// This event does the actual work.
window.addEventListener('hashchange', function() { 
	console.log('Hash: ', window.location.hash)
});

In the second step, I built a new function to scrollToAnchor(), which smoothly scrolls to the anchor element with the correct offset from the top. This function is called by the hashchanged event handler.

function scrollToAnchor(ev) {
	// Use "preventDefault" and "return false" to bypass browsers default behavior.
	ev.preventDefault();

	if (!window.location.hash) {
		return false;
	}

	var anchorTarget = $(window.location.hash);

	if (!anchorTarget.length) {
		return false;
	}

	// Let the browser finish the current process, before scrolling up/down.
	$(function() {
		window.setTimeout(function(){
			var offset = parseInt(jQuery('html').css('margin-top')) +
				parseInt(jQuery('header').first().css('height')) +
				15; // <-- This adds a variable offset!
			var anchorTop = anchorTarget.offset().top;
			anchorTop -= offset;

			// Actually copied from Divis "frontend-builder-global-functions.js"
			$( 'html, body' ).animate({ scrollTop: anchorTop });
		}, 250);
	});

	return false;
}

Full code

Here is the full JS code that can be copied into a “divi-anchor.js” file in your child theme. I do not recommend to copy-paste it into Divis option page, but enqueue the JS file the correct way.

(function($) {
window.addEventListener('DOMContentLoaded', function(ev){
	if (window.location.hash) {
		var origHash = window.location.hash;
		window.location.hash = '';

		$(function() {
			window.setTimeout(function() {
				window.location.hash = origHash;
			}, 600);
		});
	}
});

window.addEventListener('hashchange', scrollToAnchor);

function scrollToAnchor(ev){
	ev.preventDefault();

	if (window.location.hash) {
		return false;
	}

	var anchorTarget = $(window.location.hash);

	if (!anchorTarget.length) {
		return false;
	}

	// Let the browser finish the current process, before scrolling up/down.
	$(function() {
		window.setTimeout(function(){
			var offset = parseInt(jQuery('html').css('margin-top')) +
				parseInt(jQuery('header').first().css('height')) +
				15;
			var anchorTop = anchorTarget.offset().top;
			anchorTop -= offset;

			$( 'html, body' ).animate({ scrollTop: anchorTop });
		}, 250);
	});

	return false;
}
})(window.jQuery);

Simply enqueue this file in your themes functions.php, like this:

<?php

add_action( 'wp_enqueue_scripts', 'pst_theme_customize_scripts' );

/**
 * Enqueue custom JS and CSS files.
 */
function pst_theme_customize_scripts() {
	wp_enqueue_script(
		'child-theme-scroller',
		get_stylesheet_directory_uri() . '/divi-anchor.js',
		[ 'jquery' ],
		false,
		true
	);
}