Shrinking Header
A roomy header slims down once the page starts scrolling: the logo shrinks, the padding tightens and a frosted background and soft shadow fade in. Here it also floats in from the edges as a rounded pill. Scroll back to the top and it springs open again.
Scroll inside ↓
How to use it
- Paste the snippet just before
</head>. - Add
data-shrinking-headerto your header. It becomes sticky if it isn’t already. - Optionally mark the logo inside with
data-logoso it shrinks too.
Options
Add any of these attributes to the same element.
data-offset | How far the page scrolls before it compacts, in px. Default 60. |
data-padding | Top and bottom padding once compact, in px. Default 8. |
data-logo-scale | How small [data-logo] gets. Default 0.8. |
data-bg | Background once compact (it’s slightly see-through and blurs what’s behind). Default #fff. |
data-pill | Also float in from the edges as a rounded pill. |
Its roomy look is whatever your CSS says, so style the header as it should look at the top of the page. With reduced motion it still compacts, just without the movement. Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Shrinking Header: a roomy header compacts once you scroll down the page:
// it slims, its logo shrinks and it picks up a frosted background and a
// soft shadow. Scroll back to the top and it opens out again.
// Usage: <header data-shrinking-header>
// <a href="/" data-logo>Acme</a>
// <nav>…</nav>
// </header>
// Optional: data-offset (px scrolled before it compacts, default 60),
// data-padding (vertical padding when compact, in px, default 8),
// data-logo-scale (how small [data-logo] gets, default 0.8), data-bg
// (background when compact, default #fff), data-pill (also float in from
// the edges as a rounded pill).
// It makes the header sticky if it isn't already. Works inside scrolling
// boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { offset: 60, padding: 8, logoScale: 0.8, bg: '#fff', inset: 10 };
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, reduced) {
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 o = {
offset: d.offset !== undefined ? +d.offset : DEFAULTS.offset,
padding: d.padding !== undefined ? +d.padding : DEFAULTS.padding,
logoScale: +d.logoScale || DEFAULTS.logoScale,
bg: d.bg || DEFAULTS.bg,
pill: d.pill !== undefined && d.pill !== 'false'
};
// The roomy state is whatever the page's CSS says.
var padTop = parseFloat(cs.paddingTop), padBottom = parseFloat(cs.paddingBottom);
var top = parseFloat(el.style.top || cs.top) || 0;
var logo = el.querySelector('[data-logo]');
if (logo) {
logo.style.display = logo.style.display || (getComputedStyle(logo).display === 'inline' ? 'inline-block' : '');
logo.style.transformOrigin = '0 50%';
}
var box = scroller(el);
var state = { p: 0 };
var compact = false;
var anim = null;
// p runs from 0 (roomy) to 1 (compact); everything is worked out from it.
function apply() {
var p = state.p;
var q = Math.max(0, Math.min(1, p)); // the spring may overshoot; colours mustn't
el.style.paddingTop = padTop + (o.padding - padTop) * p + 'px';
el.style.paddingBottom = padBottom + (o.padding - padBottom) * p + 'px';
el.style.backgroundColor = 'color-mix(in srgb, ' + o.bg + ' ' + (q * 86).toFixed(1) + '%, transparent)';
el.style.backdropFilter = el.style.webkitBackdropFilter = 'blur(' + (q * 12).toFixed(1) + 'px) saturate(' + (1 + q * 0.6).toFixed(2) + ')';
el.style.boxShadow = '0 ' + (q * 10).toFixed(1) + 'px ' + (q * 30).toFixed(1) + 'px rgba(15, 15, 30, ' + (q * 0.12).toFixed(3) + ')';
if (logo) logo.style.transform = 'scale(' + (1 + (o.logoScale - 1) * p).toFixed(4) + ')';
if (o.pill) {
var inset = DEFAULTS.inset * q;
el.style.marginLeft = el.style.marginRight = inset + 'px';
el.style.top = top + inset + 'px';
el.style.borderRadius = (q * 999).toFixed(0) + 'px';
}
}
function check() {
var y = box ? box.scrollTop : window.scrollY;
// A little give either side of the line, so it never flutters on it.
var want = compact ? y > o.offset * 0.6 : y > o.offset;
if (want === compact) return;
compact = want;
if (anim) anim.pause();
if (reduced) { state.p = compact ? 1 : 0; apply(); return; }
anim = anime.animate(state, {
p: compact ? 1 : 0,
duration: compact ? 550 : 650,
ease: compact ? 'out(4)' : 'outElastic(1, .75)',
onUpdate: apply
});
}
apply();
check();
(box || window).addEventListener('scroll', check, { passive: true });
}
function init() {
// With reduced motion it still compacts, just without the movement.
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-shrinking-header]').forEach(function (el) {
if (el._shh) return;
el._shh = true;
setup(el, reduced);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><header data-shrinking-header>
<a href="/" data-logo>Acme</a>
<nav>…</nav>
</header>
<!-- as a floating pill: -->
<header data-shrinking-header data-pill>…</header>