Auto-hide Header
Scroll down to read and the header slides up out of the way. Scroll back up even a little and it slides straight back, ready when you need it. Near the top of the page it always shows.
How to use it
- Paste the snippet just before
</head>. - Add
data-auto-hide-headerto your header. It becomes sticky if it isn’t already.
Options
Add any of these attributes to the same element.
data-offset | How far from the top it always shows, in px. Default the header’s own height. |
data-tolerance | How far you must scroll one way before it reacts, so small jiggles don’t flick it. Default 8 px. |
data-duration | How long it takes to slide. Default 380 ms. |
data-autoplay | Inside a scrolling box, it scrolls the box down and back up by itself while in view, pausing when someone scrolls. Handy for demos. |
While it’s away the header has the class ahh-hidden, and it fires an autohide event (e.detail.hidden). Tabbing into it brings it back. Works inside scrolling boxes too: it follows the nearest scrolling parent.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Auto-hide Header: the header slides up out of the way as you scroll down
// to read, and slides back the moment you scroll up, so it's there when
// you want it and gone when you don't. Near the top of the page it always shows.
// Usage: <header data-auto-hide-header>…</header>
// It makes the header sticky if it isn't already. Works inside scrolling
// boxes too: it follows the nearest scrolling parent.
// Optional: data-offset (px from the top within which it always shows;
// default the header's height), data-tolerance (px you must scroll one way
// before it reacts, so small jiggles don't flick it; default 8),
// data-duration (ms, default 380), data-autoplay (inside a scrolling box,
// it scrolls the box down and back up by itself while in view, pausing when
// someone scrolls; handy for demos).
// The header gets the class "ahh-hidden" while it's away, and fires an
// "autohide" event (e.detail.hidden).
var DEFAULTS = { tolerance: 8, duration: 380 };
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function scroller(el) {
for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
var o = getComputedStyle(p).overflowY;
if (o === 'auto' || o === 'scroll') return p;
}
return null;
}
function setup(el) {
var d = el.dataset;
var cs = getComputedStyle(el);
if (cs.position === 'static' || cs.position === 'relative') {
el.style.position = 'sticky';
el.style.top = '0px';
}
el.style.zIndex = el.style.zIndex || '10';
var box = scroller(el);
var tolerance = d.tolerance !== undefined ? +d.tolerance : DEFAULTS.tolerance;
var duration = +d.duration || DEFAULTS.duration;
var hidden = false, last = 0, turn = 0, dir = 0;
function pos() { return box ? box.scrollTop : window.scrollY; }
function offset() { return d.offset !== undefined ? +d.offset : el.offsetHeight; }
function set(hide) {
if (hide === hidden) return;
hidden = hide;
el.classList.toggle('ahh-hidden', hide);
anime.utils.remove(el);
// Far enough up to clear its own shadow too.
var away = -(el.offsetHeight + (parseFloat(el.style.top || getComputedStyle(el).top) || 0) + 16);
if (reduced()) anime.utils.set(el, { y: hide ? away : 0 });
else anime.animate(el, { y: hide ? away : 0, duration: hide ? duration : duration * 1.1, ease: hide ? 'in(2)' : 'out(4)' });
el.dispatchEvent(new CustomEvent('autohide', { bubbles: true, detail: { hidden: hide } }));
}
function check() {
var y = pos();
var delta = y - last;
last = y;
if (!delta) return;
var now = delta > 0 ? 1 : -1;
// Measure how far it has gone since the scroll last changed direction.
if (now !== dir) { dir = now; turn = y - delta; }
if (y <= offset()) { set(false); return; }
if (Math.abs(y - turn) < tolerance) return;
set(dir > 0);
}
// Moving focus into a hidden header (keyboard users) brings it back.
el.addEventListener('focusin', function () { set(false); });
last = pos();
(box || window).addEventListener('scroll', check, { passive: true });
if (box && d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) autoplay(el, box);
}
// Scroll the box down in stretches, nudge it back up a little now and then
// (the header returns), and finally glide back to the top.
function autoplay(el, box) {
var plan = [0.3, 0.2, 0.62, 0.5, 1, 0];
var step = 0, timer = null, raf = 0, idleUntil = 0, visible = false;
var hold = function () { idleUntil = Date.now() + 8000; cancelAnimationFrame(raf); };
['wheel', 'touchstart', 'pointerdown', 'keydown'].forEach(function (t) { box.addEventListener(t, hold, { passive: true }); });
function glide(to, ms) {
var from = box.scrollTop, t0 = performance.now();
cancelAnimationFrame(raf);
var frame = function (now) {
var t = Math.min(1, (now - t0) / ms);
box.scrollTop = from + (to - from) * (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
if (t < 1) raf = requestAnimationFrame(frame);
};
raf = requestAnimationFrame(frame);
}
function tick() {
if (!visible) return;
var wait = 1500;
if (Date.now() > idleUntil && !(window.matchMedia('(hover: hover)').matches && box.matches(':hover'))) {
var max = box.scrollHeight - box.clientHeight;
var to = plan[step] * max;
var ms = Math.min(1600, 500 + Math.abs(to - box.scrollTop) * 2.5);
glide(to, ms);
wait = ms + (plan[step] === 0 ? 1100 : 800);
step = (step + 1) % plan.length;
}
timer = setTimeout(tick, wait);
}
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
clearTimeout(timer);
if (visible) timer = setTimeout(tick, 800);
else cancelAnimationFrame(raf);
}).observe(box);
}
function init() {
document.querySelectorAll('[data-auto-hide-header]').forEach(function (el) {
if (el._ahh) return;
el._ahh = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><header data-auto-hide-header>
<a href="/">Logo</a>
<nav>…</nav>
</header>