<div><span class="lbl">Shrink on scroll</span>
<div class="hdr-frame" data-scroll-root tabindex="0" role="region" aria-label="Northwind Coffee page, scrollable">
<header class="hdr hdr-shrink" data-scroll-header="shrink">
<a class="hdr-logo" href="#">Northwind</a>
<nav aria-label="Northwind"><ul><li><a href="#">Shop</a></li><li><a href="#">Subscriptions</a></li><li><a href="#">Cafés</a></li><li><a class="hdr-cart" href="#">Cart (2)</a></li></ul></nav>
</header>
<div class="hdr-body">
<h2>Single-origin beans, roasted on Mondays</h2>
<p>Every bag is roasted to order in our Leeds roastery and posted within 48 hours, so it reaches you at its best, between five and twenty days after roasting.</p>
<p>This month: a washed Ethiopian from Guji with notes of bergamot and peach, a Colombian decaf that does not taste like one, and our house espresso, now with more Brazilian body.</p>
<p>Subscriptions start at £9.50 a bag. Pause, skip or change the grind whenever you like, and the first bag ships free.</p>
<p>Our cafés in Leeds, York and Harrogate pour the same beans, and every one has a bench for laptops and a bowl of water for dogs.</p>
<p>Wholesale customers get a training day at the roastery and a technician on call for the espresso machine.</p>
<p>Questions about grind size, brew ratios or a delivery? Write to [email protected] and a person who drinks the coffee will reply.</p>
<p>Every bag carries the farm, the altitude, the process and the roast date, and the price we paid the producer per kilo.</p>
<p>We buy from eleven farms in six countries, most of them for more than five years running, and we visit each one at least every other harvest.</p>
<p>Our packaging is home-compostable, including the valve, and the boxes fit through a standard letterbox.</p>
<p>Gift subscriptions run for three, six or twelve months and arrive with a card written by hand at the roastery.</p>
<p>Training courses for home baristas run on Saturday mornings, with eight places and a lot of milk.</p>
</div>
</div></div>
<div><span class="lbl">Hide on scroll down, show on scroll up</span>
<div class="hdr-frame" data-scroll-root tabindex="0" role="region" aria-label="Tidepool News article, scrollable">
<header class="hdr hdr-hide" data-scroll-header="hide">
<a class="hdr-logo" href="#">Tidepool</a>
<nav aria-label="Tidepool sections"><ul><li><a href="#">Climate</a></li><li><a href="#">Oceans</a></li><li><a href="#">Science</a></li><li><a href="#">Subscribe</a></li></ul></nav>
</header>
<div class="hdr-body">
<h2>The kelp forests coming back to Monterey Bay</h2>
<p>Ten years after a heatwave and a plague of urchins stripped most of the bay's kelp, divers are counting new stands along the southern shore. The recovery is patchy, and it has a surprising engine: sea otters.</p>
<p>Otters eat urchins, up to a quarter of their body weight a day. Where the otters returned, the urchin barrens shrank, and the kelp, which can grow half a metre in a day, did the rest.</p>
<p>Not everywhere is so lucky. North of the bay, where otters have not spread, volunteer divers still remove urchins by hand, about 40,000 a season.</p>
<p>The researchers are cautious. One more marine heatwave could undo the gains, and the waters off California are warming faster than the global average.</p>
<p>Still, the numbers this spring were the best since 2013. The forest canopy covers about a third of its old extent, up from a tenth in 2019.</p>
<p>Scroll back up and the masthead returns, so the sections are never more than a flick away.</p>
<p>Monterey Bay Aquarium researchers now map the canopy every autumn from small planes, and the maps are published openly within a month.</p>
<p>Fishing crews who once dived for urchins as a delicacy are being paid to clear the barrens, turning a pest into a small business.</p>
<p>Kelp stores carbon, but most of it is eaten or washed ashore within a year, so scientists are careful about calling it a climate solution.</p>
<p>What it does do, without doubt, is shelter fish. Rockfish counts inside the restored stands are four times those on nearby barrens.</p>
<p>The next survey starts in May. Volunteers with a dive certificate can sign up through the aquarium.</p>
</div>
</div></div>
<div><span class="lbl">Transparent over the hero, solid after it</span>
<div class="hdr-frame" data-scroll-root tabindex="0" role="region" aria-label="Alder and Finch Architects page, scrollable">
<header class="hdr hdr-colour" data-scroll-header="colour">
<a class="hdr-logo" href="#">Alder & Finch</a>
<nav aria-label="Alder and Finch"><ul><li><a href="#">Projects</a></li><li><a href="#">Studio</a></li><li><a href="#">Contact</a></li></ul></nav>
</header>
<section class="hdr-hero" data-header-hero aria-labelledby="hdr-af-title">
<p>Architects · Edinburgh</p>
<h2 id="hdr-af-title">Buildings that are cheap to heat and good to be in</h2>
<p>Passivhaus homes, schools and workplaces across Scotland since 2009.</p>
</section>
<div class="hdr-body">
<h2>Recent work</h2>
<p>Our Portobello primary school opened in August. It uses a tenth of the heating energy of the building it replaced, and the classrooms face north so the light stays even all day.</p>
<p>In Leith, eighteen timber-framed flats for a housing co-operative are almost complete. Residents chose the layouts in four workshops with the design team.</p>
<p>We are also retrofitting a 1960s office block in Glasgow, keeping the concrete frame and wrapping it in new insulation, which saves around 1,400 tonnes of embodied carbon.</p>
<p>If you have a site or a building in mind, our first meeting is free, and we will tell you plainly whether we are the right architects for it.</p>
<p>We are a team of fourteen: architects, a Passivhaus designer, an energy modeller and a part-time ecologist who keeps us honest about biodiversity.</p>
<p>Every building we finish is monitored for two years after handover, and we publish the energy figures, including the disappointing ones.</p>
<p>Our studio is above a bakery on Leith Walk. Visitors are welcome on Fridays, and there is usually bread.</p>
</div>
</div></div>
CSS
/* Scroll-linked headers. A small script watches the scroll position of the page, or of the closest
[data-scroll-root] when the header lives in a scroll container, and toggles classes:
.is-scrolled (past a few pixels, or past the hero for the colour change) and .is-hidden (scrolling down).
CSS does the rest. A hidden header comes back when scrolling up or when anything inside it gets focus, and
under prefers-reduced-motion it never hides and nothing transitions. */
.hdr-frame{height:460px;overflow-y:auto;border-radius:18px;background:#fff;box-shadow:0 24px 50px -36px rgba(26,29,36,.5);overscroll-behavior:contain}
.hdr-frame:focus-visible{outline:3px solid #2547d0;outline-offset:3px}
.hdr{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 20px;
padding:var(--hdr-pad,22px) clamp(16px,4vw,32px);transition:padding .25s,background-color .25s,color .25s,box-shadow .25s,transform .3s}
.hdr :where(ul){margin:0;padding:0;list-style:none}
.hdr-logo{font-weight:800;font-size:var(--hdr-logo,1.45rem);letter-spacing:-.02em;color:inherit;text-decoration:none;transition:font-size .25s}
.hdr nav ul{display:flex;flex-wrap:wrap;gap:4px 18px;font-size:.92rem;font-weight:600}
.hdr nav a{color:inherit;text-decoration:none;border-radius:6px}
.hdr nav a:hover{text-decoration:underline;text-underline-offset:4px}
.hdr a:focus-visible{outline:2px solid currentColor;outline-offset:3px}
.hdr-body{padding:24px clamp(16px,4vw,32px) 40px;color:#3b4150}
.hdr-body h2{font-size:1.4rem;line-height:1.2;color:#1a1d24}
.hdr-body p{margin-top:12px;max-width:62ch}
/* 1. shrink */
.hdr-shrink{background:#fbf6ef;color:#3b2416}
.hdr-shrink.is-scrolled{--hdr-pad:10px;--hdr-logo:1.12rem;background:#fff;box-shadow:0 8px 24px -14px rgba(59,36,22,.45)}
.hdr-shrink .hdr-cart{padding:6px 12px;border-radius:999px;background:#3b2416;color:#fff}
/* 2. hide on scroll down, show on scroll up */
.hdr-hide{background:#111827;color:#f3f4f6}
.hdr-hide.is-hidden{transform:translateY(-100%)}
.hdr-hide.is-scrolled{box-shadow:0 10px 24px -14px rgba(17,24,39,.6)}
/* 3. colour change after the hero (solid until the script has run, so it is never white on white) */
.hdr-colour{margin-bottom:calc(var(--hdr-h,72px) * -1);background:#fff;color:#123b2f;box-shadow:0 8px 24px -14px rgba(18,59,47,.45)}
.hdr-colour.is-ready:not(.is-scrolled){background:linear-gradient(rgba(11,36,29,.7),rgba(11,36,29,0));color:#fff;box-shadow:none}
.hdr-hero{display:grid;align-content:end;min-height:340px;padding:96px clamp(16px,4vw,32px) 36px;background:radial-gradient(120% 90% at 85% 10%,#2f6b57,#123b2f 55%,#0b241d);color:#fff}
.hdr-hero p:first-child{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#bfe3d4}
.hdr-hero h2{margin-top:8px;max-width:16ch;font-size:clamp(1.8rem,1.3rem + 2vw,2.6rem);line-height:1.08;letter-spacing:-.02em;color:#fff}
.hdr-hero p+h2+p{margin-top:12px;max-width:52ch;color:#d5ebe2}
@media (prefers-reduced-motion:reduce){
.hdr,.hdr-logo{transition:none}
.hdr.is-hidden{transform:none}
}
JavaScript
// Toggle .is-scrolled and .is-hidden on [data-scroll-header] from the scroll position of the page or of the
// closest [data-scroll-root]. Modes (space-separated): shrink, hide, colour.
(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-scroll-header]').forEach(header => {
const root = header.closest('[data-scroll-root]');
const modes = header.dataset.scrollHeader.split(' ');
const hero = (root || document).querySelector('[data-header-hero]');
const y = () => (root ? root.scrollTop : scrollY);
let lastY = y(), queued = false;
const update = () => {
queued = false;
const now = y(), h = header.offsetHeight;
header.style.setProperty('--hdr-h', h + 'px');
header.classList.add('is-ready');
const threshold = modes.includes('colour') && hero ? hero.offsetHeight - h : 24;
header.classList.toggle('is-scrolled', now > threshold);
if (modes.includes('hide')) {
if (reduce.matches || now < h || header.contains(document.activeElement)) header.classList.remove('is-hidden');
else if (now > lastY + 4) header.classList.add('is-hidden');
else if (now < lastY - 4) header.classList.remove('is-hidden');
}
if (Math.abs(now - lastY) > 4) lastY = now;
};
(root || window).addEventListener('scroll', () => { if (!queued) { queued = true; requestAnimationFrame(update); } }, { passive: true });
header.addEventListener('focusin', () => header.classList.remove('is-hidden'));
update();
});
})();
Bootstrap 5 code
Requires: Bootstrap 5.3.8 CSS
HTML
<div><span class="lbl">Shrink on scroll</span>
<div class="hdr-frame" data-scroll-root tabindex="0" role="region" aria-label="Northwind Coffee page, scrollable">
<header class="hdr hdr-shrink sticky-top d-flex flex-wrap align-items-center justify-content-between" data-scroll-header="shrink">
<a class="hdr-logo" href="#">Northwind</a>
<nav aria-label="Northwind"><ul class="nav"><li class="nav-item"><a class="nav-link" href="#">Shop</a></li><li class="nav-item"><a class="nav-link" href="#">Subscriptions</a></li><li class="nav-item"><a class="nav-link" href="#">Cafés</a></li><li class="nav-item"><a class="nav-link hdr-cart rounded-pill" href="#">Cart (2)</a></li></ul></nav>
</header>
<div class="hdr-body">
<h2>Single-origin beans, roasted on Mondays</h2>
<p>Every bag is roasted to order in our Leeds roastery and posted within 48 hours, so it reaches you at its best, between five and twenty days after roasting.</p>
<p>This month: a washed Ethiopian from Guji with notes of bergamot and peach, a Colombian decaf that does not taste like one, and our house espresso, now with more Brazilian body.</p>
<p>Subscriptions start at £9.50 a bag. Pause, skip or change the grind whenever you like, and the first bag ships free.</p>
<p>Our cafés in Leeds, York and Harrogate pour the same beans, and every one has a bench for laptops and a bowl of water for dogs.</p>
<p>Wholesale customers get a training day at the roastery and a technician on call for the espresso machine.</p>
<p>Questions about grind size, brew ratios or a delivery? Write to [email protected] and a person who drinks the coffee will reply.</p>
<p>Every bag carries the farm, the altitude, the process and the roast date, and the price we paid the producer per kilo.</p>
<p>We buy from eleven farms in six countries, most of them for more than five years running, and we visit each one at least every other harvest.</p>
<p>Our packaging is home-compostable, including the valve, and the boxes fit through a standard letterbox.</p>
<p>Gift subscriptions run for three, six or twelve months and arrive with a card written by hand at the roastery.</p>
<p>Training courses for home baristas run on Saturday mornings, with eight places and a lot of milk.</p>
</div>
</div></div>
<div><span class="lbl">Hide on scroll down, show on scroll up</span>
<div class="hdr-frame" data-scroll-root tabindex="0" role="region" aria-label="Tidepool News article, scrollable">
<header class="hdr hdr-hide sticky-top d-flex flex-wrap align-items-center justify-content-between" data-scroll-header="hide">
<a class="hdr-logo" href="#">Tidepool</a>
<nav aria-label="Tidepool sections"><ul class="nav"><li class="nav-item"><a class="nav-link" href="#">Climate</a></li><li class="nav-item"><a class="nav-link" href="#">Oceans</a></li><li class="nav-item"><a class="nav-link" href="#">Science</a></li><li class="nav-item"><a class="nav-link" href="#">Subscribe</a></li></ul></nav>
</header>
<div class="hdr-body">
<h2>The kelp forests coming back to Monterey Bay</h2>
<p>Ten years after a heatwave and a plague of urchins stripped most of the bay's kelp, divers are counting new stands along the southern shore. The recovery is patchy, and it has a surprising engine: sea otters.</p>
<p>Otters eat urchins, up to a quarter of their body weight a day. Where the otters returned, the urchin barrens shrank, and the kelp, which can grow half a metre in a day, did the rest.</p>
<p>Not everywhere is so lucky. North of the bay, where otters have not spread, volunteer divers still remove urchins by hand, about 40,000 a season.</p>
<p>The researchers are cautious. One more marine heatwave could undo the gains, and the waters off California are warming faster than the global average.</p>
<p>Still, the numbers this spring were the best since 2013. The forest canopy covers about a third of its old extent, up from a tenth in 2019.</p>
<p>Scroll back up and the masthead returns, so the sections are never more than a flick away.</p>
<p>Monterey Bay Aquarium researchers now map the canopy every autumn from small planes, and the maps are published openly within a month.</p>
<p>Fishing crews who once dived for urchins as a delicacy are being paid to clear the barrens, turning a pest into a small business.</p>
<p>Kelp stores carbon, but most of it is eaten or washed ashore within a year, so scientists are careful about calling it a climate solution.</p>
<p>What it does do, without doubt, is shelter fish. Rockfish counts inside the restored stands are four times those on nearby barrens.</p>
<p>The next survey starts in May. Volunteers with a dive certificate can sign up through the aquarium.</p>
</div>
</div></div>
<div><span class="lbl">Transparent over the hero, solid after it</span>
<div class="hdr-frame" data-scroll-root tabindex="0" role="region" aria-label="Alder and Finch Architects page, scrollable">
<header class="hdr hdr-colour sticky-top d-flex flex-wrap align-items-center justify-content-between" data-scroll-header="colour">
<a class="hdr-logo" href="#">Alder & Finch</a>
<nav aria-label="Alder and Finch"><ul class="nav"><li class="nav-item"><a class="nav-link" href="#">Projects</a></li><li class="nav-item"><a class="nav-link" href="#">Studio</a></li><li class="nav-item"><a class="nav-link" href="#">Contact</a></li></ul></nav>
</header>
<section class="hdr-hero" data-header-hero aria-labelledby="hdr-af-title">
<p>Architects · Edinburgh</p>
<h2 id="hdr-af-title">Buildings that are cheap to heat and good to be in</h2>
<p>Passivhaus homes, schools and workplaces across Scotland since 2009.</p>
</section>
<div class="hdr-body">
<h2>Recent work</h2>
<p>Our Portobello primary school opened in August. It uses a tenth of the heating energy of the building it replaced, and the classrooms face north so the light stays even all day.</p>
<p>In Leith, eighteen timber-framed flats for a housing co-operative are almost complete. Residents chose the layouts in four workshops with the design team.</p>
<p>We are also retrofitting a 1960s office block in Glasgow, keeping the concrete frame and wrapping it in new insulation, which saves around 1,400 tonnes of embodied carbon.</p>
<p>If you have a site or a building in mind, our first meeting is free, and we will tell you plainly whether we are the right architects for it.</p>
<p>We are a team of fourteen: architects, a Passivhaus designer, an energy modeller and a part-time ecologist who keeps us honest about biodiversity.</p>
<p>Every building we finish is monitored for two years after handover, and we publish the energy figures, including the disappointing ones.</p>
<p>Our studio is above a bakery on Leith Walk. Visitors are welcome on Fridays, and there is usually bread.</p>
</div>
</div></div>
CSS
/* Scroll-linked headers in Bootstrap 5: .sticky-top, .d-flex, .nav and .nav-link for the bar, .rounded-pill and
.shadow utilities, plus the shared state classes below. A small script watches the scroll position of the page, or of the closest
[data-scroll-root] when the header lives in a scroll container, and toggles classes:
.is-scrolled (past a few pixels, or past the hero for the colour change) and .is-hidden (scrolling down).
CSS does the rest. A hidden header comes back when scrolling up or when anything inside it gets focus, and
under prefers-reduced-motion it never hides and nothing transitions. */
.hdr-frame{height:460px;overflow-y:auto;border-radius:18px;background:#fff;box-shadow:0 24px 50px -36px rgba(26,29,36,.5);overscroll-behavior:contain}
.hdr-frame:focus-visible{outline:3px solid #2547d0;outline-offset:3px}
.hdr{gap:8px 20px;
padding:var(--hdr-pad,22px) clamp(16px,4vw,32px);transition:padding .25s,background-color .25s,color .25s,box-shadow .25s,transform .3s}
.hdr-logo{font-weight:800;font-size:var(--hdr-logo,1.45rem);letter-spacing:-.02em;color:inherit;text-decoration:none;transition:font-size .25s}
.hdr .nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0;--bs-nav-link-color:inherit;--bs-nav-link-hover-color:inherit;--bs-nav-link-font-size:.92rem;--bs-nav-link-font-weight:600;gap:4px 18px;align-items:center}
.hdr .nav-link{border-radius:6px}
.hdr .nav-link:hover{text-decoration:underline;text-underline-offset:4px}
.hdr a:focus-visible{outline:2px solid currentColor;outline-offset:3px;box-shadow:none}
.hdr-body{padding:24px clamp(16px,4vw,32px) 40px;color:#3b4150}
.hdr-body h2{font-size:1.4rem;font-weight:700;line-height:1.2;color:#1a1d24;margin:0}
.hdr-body p{margin:12px 0 0;max-width:62ch}
/* 1. shrink */
.hdr-shrink{background:#fbf6ef;color:#3b2416}
.hdr-shrink.is-scrolled{--hdr-pad:10px;--hdr-logo:1.12rem;background:#fff;box-shadow:0 8px 24px -14px rgba(59,36,22,.45)}
.hdr-shrink .hdr-cart{--bs-nav-link-hover-color:#fff;padding:6px 12px;background:#3b2416;color:#fff}
/* 2. hide on scroll down, show on scroll up */
.hdr-hide{background:#111827;color:#f3f4f6}
.hdr-hide.is-hidden{transform:translateY(-100%)}
.hdr-hide.is-scrolled{box-shadow:0 10px 24px -14px rgba(17,24,39,.6)}
/* 3. colour change after the hero (solid until the script has run, so it is never white on white) */
.hdr-colour{margin-bottom:calc(var(--hdr-h,72px) * -1);background:#fff;color:#123b2f;box-shadow:0 8px 24px -14px rgba(18,59,47,.45)}
.hdr-colour.is-ready:not(.is-scrolled){background:linear-gradient(rgba(11,36,29,.7),rgba(11,36,29,0));color:#fff;box-shadow:none}
.hdr-hero{display:grid;align-content:end;min-height:340px;padding:96px clamp(16px,4vw,32px) 36px;background:radial-gradient(120% 90% at 85% 10%,#2f6b57,#123b2f 55%,#0b241d);color:#fff}
.hdr-hero p{margin:0}
.hdr-hero p:first-child{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#bfe3d4}
.hdr-hero h2{margin:8px 0 0;font-weight:700;max-width:16ch;font-size:clamp(1.8rem,1.3rem + 2vw,2.6rem);line-height:1.08;letter-spacing:-.02em;color:#fff}
.hdr-hero p+h2+p{margin-top:12px;max-width:52ch;color:#d5ebe2}
@media (prefers-reduced-motion:reduce){
.hdr,.hdr-logo{transition:none}
.hdr.is-hidden{transform:none}
}
JavaScript
// Toggle .is-scrolled and .is-hidden on [data-scroll-header] from the scroll position of the page or of the
// closest [data-scroll-root]. Modes (space-separated): shrink, hide, colour.
(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-scroll-header]').forEach(header => {
const root = header.closest('[data-scroll-root]');
const modes = header.dataset.scrollHeader.split(' ');
const hero = (root || document).querySelector('[data-header-hero]');
const y = () => (root ? root.scrollTop : scrollY);
let lastY = y(), queued = false;
const update = () => {
queued = false;
const now = y(), h = header.offsetHeight;
header.style.setProperty('--hdr-h', h + 'px');
header.classList.add('is-ready');
const threshold = modes.includes('colour') && hero ? hero.offsetHeight - h : 24;
header.classList.toggle('is-scrolled', now > threshold);
if (modes.includes('hide')) {
if (reduce.matches || now < h || header.contains(document.activeElement)) header.classList.remove('is-hidden');
else if (now > lastY + 4) header.classList.add('is-hidden');
else if (now < lastY - 4) header.classList.remove('is-hidden');
}
if (Math.abs(now - lastY) > 4) lastY = now;
};
(root || window).addEventListener('scroll', () => { if (!queued) { queued = true; requestAnimationFrame(update); } }, { passive: true });
header.addEventListener('focusin', () => header.classList.remove('is-hidden'));
update();
});
})();
Tailwind 4 code
Requires: Tailwind CSS 4
HTML
<!-- Utilities only: the script toggles .is-scrolled, .is-hidden and .is-ready on the header, and the
[&.is-scrolled]:, [&.is-hidden]: and group-[.is-scrolled]: variants style each state. -->
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#555c6b] uppercase">Shrink on scroll</span>
<div class="h-[460px] overflow-y-auto overscroll-contain rounded-[18px] bg-white shadow-[0_24px_50px_-36px_rgba(26,29,36,.5)] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#2547d0]" data-scroll-root tabindex="0" role="region" aria-label="Northwind Coffee page, scrollable">
<header class="group sticky top-0 z-10 flex flex-wrap items-center justify-between gap-x-5 gap-y-2 px-[clamp(16px,4vw,32px)] py-[22px] transition-[padding,background-color,color,box-shadow,transform] duration-300 motion-reduce:transition-none bg-[#fbf6ef] text-[#3b2416] [&.is-scrolled]:bg-white [&.is-scrolled]:py-2.5 [&.is-scrolled]:shadow-[0_8px_24px_-14px_rgba(59,36,22,.45)]" data-scroll-header="shrink">
<a class="text-[1.45rem] font-extrabold tracking-[-.02em] transition-[font-size] duration-300 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current motion-reduce:transition-none group-[.is-scrolled]:text-[1.12rem]" href="#">Northwind</a>
<nav aria-label="Northwind"><ul class="flex flex-wrap gap-x-[18px] gap-y-1 text-[.92rem] font-semibold"><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Shop</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Subscriptions</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Cafés</a></li><li><a class="rounded-full bg-[#3b2416] px-3 py-1.5 text-white hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Cart (2)</a></li></ul></nav>
</header>
<div class="px-[clamp(16px,4vw,32px)] pt-6 pb-10 text-[#3b4150]">
<h2 class="text-[1.4rem] leading-[1.2] font-bold text-[#1a1d24]">Single-origin beans, roasted on Mondays</h2>
<p class="mt-3 max-w-[62ch]">Every bag is roasted to order in our Leeds roastery and posted within 48 hours, so it reaches you at its best, between five and twenty days after roasting.</p>
<p class="mt-3 max-w-[62ch]">This month: a washed Ethiopian from Guji with notes of bergamot and peach, a Colombian decaf that does not taste like one, and our house espresso, now with more Brazilian body.</p>
<p class="mt-3 max-w-[62ch]">Subscriptions start at £9.50 a bag. Pause, skip or change the grind whenever you like, and the first bag ships free.</p>
<p class="mt-3 max-w-[62ch]">Our cafés in Leeds, York and Harrogate pour the same beans, and every one has a bench for laptops and a bowl of water for dogs.</p>
<p class="mt-3 max-w-[62ch]">Wholesale customers get a training day at the roastery and a technician on call for the espresso machine.</p>
<p class="mt-3 max-w-[62ch]">Questions about grind size, brew ratios or a delivery? Write to [email protected] and a person who drinks the coffee will reply.</p>
<p class="mt-3 max-w-[62ch]">Every bag carries the farm, the altitude, the process and the roast date, and the price we paid the producer per kilo.</p>
<p class="mt-3 max-w-[62ch]">We buy from eleven farms in six countries, most of them for more than five years running, and we visit each one at least every other harvest.</p>
<p class="mt-3 max-w-[62ch]">Our packaging is home-compostable, including the valve, and the boxes fit through a standard letterbox.</p>
<p class="mt-3 max-w-[62ch]">Gift subscriptions run for three, six or twelve months and arrive with a card written by hand at the roastery.</p>
<p class="mt-3 max-w-[62ch]">Training courses for home baristas run on Saturday mornings, with eight places and a lot of milk.</p>
</div>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#555c6b] uppercase">Hide on scroll down, show on scroll up</span>
<div class="h-[460px] overflow-y-auto overscroll-contain rounded-[18px] bg-white shadow-[0_24px_50px_-36px_rgba(26,29,36,.5)] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#2547d0]" data-scroll-root tabindex="0" role="region" aria-label="Tidepool News article, scrollable">
<header class="group sticky top-0 z-10 flex flex-wrap items-center justify-between gap-x-5 gap-y-2 px-[clamp(16px,4vw,32px)] py-[22px] transition-[padding,background-color,color,box-shadow,transform] duration-300 motion-reduce:transition-none bg-[#111827] text-[#f3f4f6] [&.is-hidden]:-translate-y-full motion-reduce:[&.is-hidden]:translate-y-0 [&.is-scrolled]:shadow-[0_10px_24px_-14px_rgba(17,24,39,.6)]" data-scroll-header="hide">
<a class="text-[1.45rem] font-extrabold tracking-[-.02em] transition-[font-size] duration-300 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current motion-reduce:transition-none group-[.is-scrolled]:text-[1.12rem]" href="#">Tidepool</a>
<nav aria-label="Tidepool sections"><ul class="flex flex-wrap gap-x-[18px] gap-y-1 text-[.92rem] font-semibold"><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Climate</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Oceans</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Science</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Subscribe</a></li></ul></nav>
</header>
<div class="px-[clamp(16px,4vw,32px)] pt-6 pb-10 text-[#3b4150]">
<h2 class="text-[1.4rem] leading-[1.2] font-bold text-[#1a1d24]">The kelp forests coming back to Monterey Bay</h2>
<p class="mt-3 max-w-[62ch]">Ten years after a heatwave and a plague of urchins stripped most of the bay's kelp, divers are counting new stands along the southern shore. The recovery is patchy, and it has a surprising engine: sea otters.</p>
<p class="mt-3 max-w-[62ch]">Otters eat urchins, up to a quarter of their body weight a day. Where the otters returned, the urchin barrens shrank, and the kelp, which can grow half a metre in a day, did the rest.</p>
<p class="mt-3 max-w-[62ch]">Not everywhere is so lucky. North of the bay, where otters have not spread, volunteer divers still remove urchins by hand, about 40,000 a season.</p>
<p class="mt-3 max-w-[62ch]">The researchers are cautious. One more marine heatwave could undo the gains, and the waters off California are warming faster than the global average.</p>
<p class="mt-3 max-w-[62ch]">Still, the numbers this spring were the best since 2013. The forest canopy covers about a third of its old extent, up from a tenth in 2019.</p>
<p class="mt-3 max-w-[62ch]">Scroll back up and the masthead returns, so the sections are never more than a flick away.</p>
<p class="mt-3 max-w-[62ch]">Monterey Bay Aquarium researchers now map the canopy every autumn from small planes, and the maps are published openly within a month.</p>
<p class="mt-3 max-w-[62ch]">Fishing crews who once dived for urchins as a delicacy are being paid to clear the barrens, turning a pest into a small business.</p>
<p class="mt-3 max-w-[62ch]">Kelp stores carbon, but most of it is eaten or washed ashore within a year, so scientists are careful about calling it a climate solution.</p>
<p class="mt-3 max-w-[62ch]">What it does do, without doubt, is shelter fish. Rockfish counts inside the restored stands are four times those on nearby barrens.</p>
<p class="mt-3 max-w-[62ch]">The next survey starts in May. Volunteers with a dive certificate can sign up through the aquarium.</p>
</div>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#555c6b] uppercase">Transparent over the hero, solid after it</span>
<div class="h-[460px] overflow-y-auto overscroll-contain rounded-[18px] bg-white shadow-[0_24px_50px_-36px_rgba(26,29,36,.5)] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#2547d0]" data-scroll-root tabindex="0" role="region" aria-label="Alder and Finch Architects page, scrollable">
<header class="group sticky top-0 z-10 flex flex-wrap items-center justify-between gap-x-5 gap-y-2 px-[clamp(16px,4vw,32px)] py-[22px] transition-[padding,background-color,color,box-shadow,transform] duration-300 motion-reduce:transition-none -mb-[var(--hdr-h,72px)] bg-white text-[#123b2f] shadow-[0_8px_24px_-14px_rgba(18,59,47,.45)] [&.is-ready:not(.is-scrolled)]:bg-[linear-gradient(rgba(11,36,29,.7),rgba(11,36,29,0))] [&.is-ready:not(.is-scrolled)]:text-white [&.is-ready:not(.is-scrolled)]:shadow-none" data-scroll-header="colour">
<a class="text-[1.45rem] font-extrabold tracking-[-.02em] transition-[font-size] duration-300 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current motion-reduce:transition-none group-[.is-scrolled]:text-[1.12rem]" href="#">Alder & Finch</a>
<nav aria-label="Alder and Finch"><ul class="flex flex-wrap gap-x-[18px] gap-y-1 text-[.92rem] font-semibold"><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Projects</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Studio</a></li><li><a class="rounded-md hover:underline hover:underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-current" href="#">Contact</a></li></ul></nav>
</header>
<section class="grid min-h-[340px] content-end bg-[radial-gradient(120%_90%_at_85%_10%,#2f6b57,#123b2f_55%,#0b241d)] px-[clamp(16px,4vw,32px)] pt-24 pb-9 text-white" data-header-hero aria-labelledby="hdr-af-title">
<p class="text-[.78rem] font-bold tracking-[.14em] text-[#bfe3d4] uppercase">Architects · Edinburgh</p>
<h2 class="mt-2 max-w-[16ch] text-[clamp(1.8rem,1.3rem+2vw,2.6rem)] leading-[1.08] font-bold tracking-[-.02em]" id="hdr-af-title">Buildings that are cheap to heat and good to be in</h2>
<p class="mt-3 max-w-[52ch] text-[#d5ebe2]">Passivhaus homes, schools and workplaces across Scotland since 2009.</p>
</section>
<div class="px-[clamp(16px,4vw,32px)] pt-6 pb-10 text-[#3b4150]">
<h2 class="text-[1.4rem] leading-[1.2] font-bold text-[#1a1d24]">Recent work</h2>
<p class="mt-3 max-w-[62ch]">Our Portobello primary school opened in August. It uses a tenth of the heating energy of the building it replaced, and the classrooms face north so the light stays even all day.</p>
<p class="mt-3 max-w-[62ch]">In Leith, eighteen timber-framed flats for a housing co-operative are almost complete. Residents chose the layouts in four workshops with the design team.</p>
<p class="mt-3 max-w-[62ch]">We are also retrofitting a 1960s office block in Glasgow, keeping the concrete frame and wrapping it in new insulation, which saves around 1,400 tonnes of embodied carbon.</p>
<p class="mt-3 max-w-[62ch]">If you have a site or a building in mind, our first meeting is free, and we will tell you plainly whether we are the right architects for it.</p>
<p class="mt-3 max-w-[62ch]">We are a team of fourteen: architects, a Passivhaus designer, an energy modeller and a part-time ecologist who keeps us honest about biodiversity.</p>
<p class="mt-3 max-w-[62ch]">Every building we finish is monitored for two years after handover, and we publish the energy figures, including the disappointing ones.</p>
<p class="mt-3 max-w-[62ch]">Our studio is above a bakery on Leith Walk. Visitors are welcome on Fridays, and there is usually bread.</p>
</div>
</div></div>
JavaScript
// Toggle .is-scrolled and .is-hidden on [data-scroll-header] from the scroll position of the page or of the
// closest [data-scroll-root]. Modes (space-separated): shrink, hide, colour.
(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-scroll-header]').forEach(header => {
const root = header.closest('[data-scroll-root]');
const modes = header.dataset.scrollHeader.split(' ');
const hero = (root || document).querySelector('[data-header-hero]');
const y = () => (root ? root.scrollTop : scrollY);
let lastY = y(), queued = false;
const update = () => {
queued = false;
const now = y(), h = header.offsetHeight;
header.style.setProperty('--hdr-h', h + 'px');
header.classList.add('is-ready');
const threshold = modes.includes('colour') && hero ? hero.offsetHeight - h : 24;
header.classList.toggle('is-scrolled', now > threshold);
if (modes.includes('hide')) {
if (reduce.matches || now < h || header.contains(document.activeElement)) header.classList.remove('is-hidden');
else if (now > lastY + 4) header.classList.add('is-hidden');
else if (now < lastY - 4) header.classList.remove('is-hidden');
}
if (Math.abs(now - lastY) > 4) lastY = now;
};
(root || window).addEventListener('scroll', () => { if (!queued) { queued = true; requestAnimationFrame(update); } }, { passive: true });
header.addEventListener('focusin', () => header.classList.remove('is-hidden'));
update();
});
})();
About this set
Three sticky header behaviours, each shown in its own scrolling window so you can try them side by side. Shrink on scroll is a coffee roaster header that reduces its padding and logo size, turns white and gains a shadow once the page has moved a few pixels. Hide on scroll down, show on scroll up is a news masthead that slides out of the way while you read and comes back as soon as you scroll up, or as soon as anything inside it gets keyboard focus. Transparent over the hero, solid after it is an architecture practice header with white text on a dark gradient hero that switches to a white bar with dark text once the hero has passed. One small script handles all three: it reads the scroll position of the page or of the closest scroll container marked data-scroll-root, throttles with requestAnimationFrame and toggles is-scrolled and is-hidden classes, and the CSS does the rest. The colour-change header stays solid until the script has run, so it can never end up as white text on a white page.
What’s included
Shrink, hide on scroll down and transparent-to-solid headers
One requestAnimationFrame-throttled script, set by a data attribute
Works for the page or any scroll container
Hidden header returns on scroll up or keyboard focus
Never hides and never animates under reduced motion
Accessibility
Each header is a header element with a named nav, real links and a visible focus outline in the current text colour. The hiding header comes back whenever focus moves into it, so keyboard users never tab into an invisible bar. Both header states use colour pairs above 4.5:1, and the transparent state has a dark gradient behind the links. Under prefers-reduced-motion the header never hides and every change is instant.
Customise it
Change --hdr-pad and --hdr-logo in the .is-scrolled rule for the shrink amount, the background and colour pairs for each state, and the 24px threshold in the script. In Tailwind the states are [&.is-scrolled]:, [&.is-hidden]: and group-[.is-scrolled]: variants.