WordPress Anchor Offset

The Problem

If you’re using Gutenberg for WordPress and taking advantage of their handy-dandy anchor tools, but you also like your headers to be fixed or sticky, then you’ve probably noticed strange behavior where the element you’re scrolling to is hidden behind the header. Okay, this isn’t just a problem with WordPress. It can be a problem anywhere.

Here’s a sample of scrolling down to #header-1. The content you want to be focused is hidden.

screenshot of anchor issue with fixed header

The Solution

I’ve read a lot of solutions that are just too complicated. I’ve seen lengthy JavaScript solutions. I’ve seen CSS solutions that require you to set a hidden margin/padding in a pseudo element before the ID. Maybe that’s okay if you know where those anchors are going to be across the whole site. But for a dynamic site like WordPress? Gross. The answer is scroll-margin-top. If you know the height of your header just set the value to that height.

[id] {
	scroll-margin-top: 80px;
}

That’s it!

Optional JavaScript Solution

If the height of your header varies you can use JavaScript to determine the height of the header and then set the anchor offset. Here’s a sample in plain JavaScript written by Daniel that checks the header height on resize. This may be overkill for you, but this covers everything we need.

(function () {
	let header = null,
		styleEl = null,
		CSSSelector = '[id]';

	function docReady(fn) {
		if (document.readyState != 'loading') {
			fn();
		} else {
			document.addEventListener('DOMContentLoaded', fn);
		}
	}
	
	function debounce(fn, ms) {
		let timeout = false;
	
		return function () {
			if (timeout) {
				clearTimeout(timeout);
			}
	
			timeout = setTimeout(() => {
				fn.apply(this, Array.prototype.slice.call(arguments));
			}, ms);
		};
	}
	
	function resize() {
		let style = getComputedStyle(header);

		if (style.position == 'fixed') {
			if (!styleEl) {
				styleEl = document.createElement('style');
				document.head.appendChild(styleEl);
			}
			/* sets the value of scroll-margin-top to the height of the header, plus 30px so the anchor isn't right on top' */
			let css = 'scroll-margin-top:' + (parseFloat(style.height) + 30) + 'px;';
			styleEl.innerHTML = CSSSelector + '{' + css + '}';
		}
		else {
			if (styleEl) {
				document.head.removeChild(styleEl);
				styleEl = null;
			}
		}
	}
	
	docReady(function () {
		header = document.querySelector('.header');
	
		if (header) {
			window.addEventListener('resize', debounce(resize, 300));
			resize();
		}
	});
})();

On line 37 we’re adding an extra 30px. You can remove/edit that if it fits your theme better. Ideally we’d make a plugin that adds a field to set additional offset on a case-by-case basis. Someday.