39 CSS Breadcrumbs07 / 39
Sticky Top CSS Navigation Header with Shrinking Breadcrumb Trail
A sticky page header whose breadcrumb trail shrinks as you scroll — font size, padding and vertical footprint all contract into a compact bar that stays pinned to the top. Modern build uses CSS scroll-driven animation-timeline:scroll(); a tiny IntersectionObserver fallback ships for engines without it.
Published
The code
<div class="bc-07" data-shrunk="false">
<header class="bc-07__bar">
<span class="bc-07__logo" aria-hidden="true"></span>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#" aria-current="page">Scroll-driven UI</a></li>
</ol>
</nav>
</header>
<div class="bc-07__sentinel" aria-hidden="true"></div>
<div class="bc-07__content">
<h1 class="bc-07__h1">Scroll-driven sticky breadcrumb header</h1>
<p class="bc-07__lede">Scroll this panel ↓ and watch the sticky breadcrumb header at the top compress into a slim pinned bar. Padding, type scale, and logo size all contract via CSS <code>animation-timeline: scroll()</code> — no JavaScript scroll listener needed.</p>
<h2 class="bc-07__h2">How the shrink is wired</h2>
<p>Modern browsers support scroll-driven animations natively (Chrome 115+, Safari 26+, Firefox 134+). The trick is one <code>@keyframes</code> block plus <code>animation-timeline: scroll(nearest)</code> on the sticky header. As the user scrolls past a sentinel div, the timeline advances from 0 to 1 and the browser interpolates the keyframe values.</p>
<p>This is the exact pattern <strong>Docusaurus 3</strong>, <strong>Mintlify</strong>, <strong>Nextra</strong>, <strong>VitePress</strong>, <strong>Starlight</strong>, and <strong>Fumadocs</strong> ship on their generated docs sites. No IntersectionObserver, no scroll event listener, no rAF throttle needed — the browser’s own compositor drives the animation.</p>
<h2 class="bc-07__h2">Accessibility contract</h2>
<p>The breadcrumb still needs to satisfy WCAG 2.4.8 (Location) and WCAG 1.4.3 (Contrast Minimum) regardless of shrink state. Both expanded and compact states use a token-based color system that maintains AA contrast against the frosted-glass backdrop.</p>
<p>Motion is gated behind <code>@media (prefers-reduced-motion: no-preference)</code> — users who opted out of motion at the OS level see a static bar at the compact size, never the animation. This is WCAG 2.3.3 (Animation from Interactions) compliance.</p>
<h2 class="bc-07__h2">Falling back gracefully</h2>
<p>Older Safari and Firefox versions that don’t support <code>animation-timeline: scroll()</code> just render the base state — a sticky bar with the expanded padding. The demo also ships a 12-line IntersectionObserver fallback that toggles a <code>.is-shrunk</code> class when the sentinel exits the viewport, so the shrink still works for the ~5% of users on older engines.</p>
<p>Feature-detect once at page load: <code>if (CSS.supports('animation-timeline', 'scroll()')) { /* skip JS fallback */ }</code>. Ship both paths; let the browser pick.</p>
<h2 class="bc-07__h2">Try it: scroll to the bottom</h2>
<p>The last paragraph sits at the far bottom so you can watch the header shrink smoothly as the sentinel scrolls out of view. Grab the scrollbar or two-finger swipe to see the full range of the animation.</p>
<p>Keep scrolling ↓</p>
<p>The shrink kicks in at the sentinel boundary, then holds at the compact size all the way to the bottom.</p>
<p>Once you scroll back up past the sentinel, the header expands smoothly back to its original size — the animation is bidirectional.</p>
<p>The frosted-glass <code>backdrop-filter</code> stays consistent throughout, so body text never fights the header for contrast.</p>
<p>This is the scroll-driven pattern behind every modern docs-site UX from 2024 onward.</p>
<p>End of scroll region.</p>
</div>
</div><div class="bc-07" data-shrunk="false">
<header class="bc-07__bar">
<span class="bc-07__logo" aria-hidden="true"></span>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#" aria-current="page">Scroll-driven UI</a></li>
</ol>
</nav>
</header>
<div class="bc-07__sentinel" aria-hidden="true"></div>
<div class="bc-07__content">
<h1 class="bc-07__h1">Scroll-driven sticky breadcrumb header</h1>
<p class="bc-07__lede">Scroll this panel ↓ and watch the sticky breadcrumb header at the top compress into a slim pinned bar. Padding, type scale, and logo size all contract via CSS <code>animation-timeline: scroll()</code> — no JavaScript scroll listener needed.</p>
<h2 class="bc-07__h2">How the shrink is wired</h2>
<p>Modern browsers support scroll-driven animations natively (Chrome 115+, Safari 26+, Firefox 134+). The trick is one <code>@keyframes</code> block plus <code>animation-timeline: scroll(nearest)</code> on the sticky header. As the user scrolls past a sentinel div, the timeline advances from 0 to 1 and the browser interpolates the keyframe values.</p>
<p>This is the exact pattern <strong>Docusaurus 3</strong>, <strong>Mintlify</strong>, <strong>Nextra</strong>, <strong>VitePress</strong>, <strong>Starlight</strong>, and <strong>Fumadocs</strong> ship on their generated docs sites. No IntersectionObserver, no scroll event listener, no rAF throttle needed — the browser’s own compositor drives the animation.</p>
<h2 class="bc-07__h2">Accessibility contract</h2>
<p>The breadcrumb still needs to satisfy WCAG 2.4.8 (Location) and WCAG 1.4.3 (Contrast Minimum) regardless of shrink state. Both expanded and compact states use a token-based color system that maintains AA contrast against the frosted-glass backdrop.</p>
<p>Motion is gated behind <code>@media (prefers-reduced-motion: no-preference)</code> — users who opted out of motion at the OS level see a static bar at the compact size, never the animation. This is WCAG 2.3.3 (Animation from Interactions) compliance.</p>
<h2 class="bc-07__h2">Falling back gracefully</h2>
<p>Older Safari and Firefox versions that don’t support <code>animation-timeline: scroll()</code> just render the base state — a sticky bar with the expanded padding. The demo also ships a 12-line IntersectionObserver fallback that toggles a <code>.is-shrunk</code> class when the sentinel exits the viewport, so the shrink still works for the ~5% of users on older engines.</p>
<p>Feature-detect once at page load: <code>if (CSS.supports('animation-timeline', 'scroll()')) { /* skip JS fallback */ }</code>. Ship both paths; let the browser pick.</p>
<h2 class="bc-07__h2">Try it: scroll to the bottom</h2>
<p>The last paragraph sits at the far bottom so you can watch the header shrink smoothly as the sentinel scrolls out of view. Grab the scrollbar or two-finger swipe to see the full range of the animation.</p>
<p>Keep scrolling ↓</p>
<p>The shrink kicks in at the sentinel boundary, then holds at the compact size all the way to the bottom.</p>
<p>Once you scroll back up past the sentinel, the header expands smoothly back to its original size — the animation is bidirectional.</p>
<p>The frosted-glass <code>backdrop-filter</code> stays consistent throughout, so body text never fights the header for contrast.</p>
<p>This is the scroll-driven pattern behind every modern docs-site UX from 2024 onward.</p>
<p>End of scroll region.</p>
</div>
</div>.bc-07 {
display: grid;
width: 100%;
min-height: 100vh;
font-family: system-ui,'Segoe UI',sans-serif;
height: 300px;
overflow-y: auto;
background: oklch(0.97 0.006 255);
position: relative;
scrollbar-width: thin;
place-items: center;
}
.bc-07 * {
box-sizing: border-box;
}
.bc-07__bar {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 14px;
padding: 20px 24px;
background: oklch(1 0 0/.85);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-bottom: 1px solid oklch(0.9 0.01 255);
box-shadow: 0 0 0 rgba(0,0,0,0);
transition: padding .3s ease,box-shadow .3s ease;
}
.bc-07__logo {
width: 34px;
height: 34px;
flex: none;
border-radius: 9px;
background: conic-gradient(from 210deg,oklch(0.62 0.17 265),oklch(0.6 0.16 210));
transition: width .3s ease,height .3s ease;
}
.bc-07 ol {
display: flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.bc-07 li {
display: flex;
align-items: center;
}
.bc-07 li + li::before {
content: "›";
margin: 0 8px;
color: oklch(0.72 0.02 255);
}
.bc-07 a {
font: 600 15px/1 system-ui,sans-serif;
color: oklch(0.5 0.1 260);
text-decoration: none;
padding: 3px 5px;
border-radius: 6px;
transition: font-size .3s ease,color .15s ease;
}
.bc-07 a:hover {
color: oklch(0.42 0.14 260);
}
.bc-07 a:focus-visible {
outline: 2px solid oklch(0.5 0.15 260);
outline-offset: 2px;
}
.bc-07 li[aria-current="page"] a {
color: oklch(0.28 0.02 255);
font-weight: 700;
}
.bc-07[data-shrunk="true"] .bc-07__bar {
padding: 9px 24px;
box-shadow: 0 4px 18px rgba(0,0,0,.1);
}
.bc-07[data-shrunk="true"] .bc-07__logo {
width: 24px;
height: 24px;
}
.bc-07[data-shrunk="true"] .bc-07 a {
font-size: 13px;
}
.bc-07__sentinel {
height: 1px;
}
@media (prefers-reduced-motion: reduce) {
.bc-07__bar,
.bc-07__logo,
.bc-07 a {
transition: none;
}
}
.bc-07 .bc-07__h1 {
font: 700 22px/1.3 system-ui,sans-serif;
color: oklch(0.25 0.02 255);
margin: 0 0 8px;
}
.bc-07 .bc-07__h2 {
font: 700 15px/1.3 system-ui,sans-serif;
color: oklch(0.3 0.02 255);
margin: 24px 0 6px;
}
.bc-07 .bc-07__lede {
font: 500 15px/1.55 system-ui,sans-serif;
color: oklch(0.42 0.02 255);
margin: 0 0 20px;
max-width: 60ch;
}
.bc-07 .bc-07__content {
padding: 24px 24px 40px;
}
.bc-07 .bc-07__content p {
margin: 0 0 14px;
color: oklch(0.42 0.02 255);
font-size: 14px;
line-height: 1.65;
max-width: 60ch;
}
.bc-07 .bc-07__content code {
font: 500 12.5px/1 ui-monospace,Menlo,monospace;
color: oklch(0.35 0.05 260);
background: oklch(0.94 0.01 255);
padding: 2px 6px;
border-radius: 4px;
}
.bc-07 .bc-07__content strong {
color: oklch(0.28 0.02 255);
font-weight: 700;
}.bc-07 {
display: grid;
width: 100%;
min-height: 100vh;
font-family: system-ui,'Segoe UI',sans-serif;
height: 300px;
overflow-y: auto;
background: oklch(0.97 0.006 255);
position: relative;
scrollbar-width: thin;
place-items: center;
}
.bc-07 * {
box-sizing: border-box;
}
.bc-07__bar {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 14px;
padding: 20px 24px;
background: oklch(1 0 0/.85);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-bottom: 1px solid oklch(0.9 0.01 255);
box-shadow: 0 0 0 rgba(0,0,0,0);
transition: padding .3s ease,box-shadow .3s ease;
}
.bc-07__logo {
width: 34px;
height: 34px;
flex: none;
border-radius: 9px;
background: conic-gradient(from 210deg,oklch(0.62 0.17 265),oklch(0.6 0.16 210));
transition: width .3s ease,height .3s ease;
}
.bc-07 ol {
display: flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.bc-07 li {
display: flex;
align-items: center;
}
.bc-07 li + li::before {
content: "›";
margin: 0 8px;
color: oklch(0.72 0.02 255);
}
.bc-07 a {
font: 600 15px/1 system-ui,sans-serif;
color: oklch(0.5 0.1 260);
text-decoration: none;
padding: 3px 5px;
border-radius: 6px;
transition: font-size .3s ease,color .15s ease;
}
.bc-07 a:hover {
color: oklch(0.42 0.14 260);
}
.bc-07 a:focus-visible {
outline: 2px solid oklch(0.5 0.15 260);
outline-offset: 2px;
}
.bc-07 li[aria-current="page"] a {
color: oklch(0.28 0.02 255);
font-weight: 700;
}
.bc-07[data-shrunk="true"] .bc-07__bar {
padding: 9px 24px;
box-shadow: 0 4px 18px rgba(0,0,0,.1);
}
.bc-07[data-shrunk="true"] .bc-07__logo {
width: 24px;
height: 24px;
}
.bc-07[data-shrunk="true"] .bc-07 a {
font-size: 13px;
}
.bc-07__sentinel {
height: 1px;
}
@media (prefers-reduced-motion: reduce) {
.bc-07__bar,
.bc-07__logo,
.bc-07 a {
transition: none;
}
}
.bc-07 .bc-07__h1 {
font: 700 22px/1.3 system-ui,sans-serif;
color: oklch(0.25 0.02 255);
margin: 0 0 8px;
}
.bc-07 .bc-07__h2 {
font: 700 15px/1.3 system-ui,sans-serif;
color: oklch(0.3 0.02 255);
margin: 24px 0 6px;
}
.bc-07 .bc-07__lede {
font: 500 15px/1.55 system-ui,sans-serif;
color: oklch(0.42 0.02 255);
margin: 0 0 20px;
max-width: 60ch;
}
.bc-07 .bc-07__content {
padding: 24px 24px 40px;
}
.bc-07 .bc-07__content p {
margin: 0 0 14px;
color: oklch(0.42 0.02 255);
font-size: 14px;
line-height: 1.65;
max-width: 60ch;
}
.bc-07 .bc-07__content code {
font: 500 12.5px/1 ui-monospace,Menlo,monospace;
color: oklch(0.35 0.05 260);
background: oklch(0.94 0.01 255);
padding: 2px 6px;
border-radius: 4px;
}
.bc-07 .bc-07__content strong {
color: oklch(0.28 0.02 255);
font-weight: 700;
}/* Scroll-shrink via IntersectionObserver (universal fallback for animation-timeline:scroll()) */
document.querySelectorAll('.bc-07').forEach((root) => {
const sentinel = root.querySelector('.bc-07__sentinel');
const io = new IntersectionObserver(
([e]) => root.setAttribute('data-shrunk', String(!e.isIntersecting)),
{ root, threshold: 0 }
);
io.observe(sentinel);
});
/* Pure-CSS alternative: give .bc-07__bar { animation: shrink linear both; animation-timeline: scroll(nearest); } and keyframe the same properties. *//* Scroll-shrink via IntersectionObserver (universal fallback for animation-timeline:scroll()) */
document.querySelectorAll('.bc-07').forEach((root) => {
const sentinel = root.querySelector('.bc-07__sentinel');
const io = new IntersectionObserver(
([e]) => root.setAttribute('data-shrunk', String(!e.isIntersecting)),
{ root, threshold: 0 }
);
io.observe(sentinel);
});
/* Pure-CSS alternative: give .bc-07__bar { animation: shrink linear both; animation-timeline: scroll(nearest); } and keyframe the same properties. */Here's a working CSS Breadcrumb from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Sticky Top CSS Navigation Header with Shrinking Breadcrumb Trail
Source: https://codefronts.com/navigation/css-breadcrumbs/sticky-top-css-navigation-header-with-shrinking-breadcrumb-trail/
A sticky page header whose breadcrumb trail shrinks as you scroll — font size, padding and vertical footprint all contract into a compact bar that stays pinned to the top. Modern build uses CSS scroll-driven animation-timeline:scroll(); a tiny IntersectionObserver fallback ships for engines without it.
## HTML
```html
<div class="bc-07" data-shrunk="false">
<header class="bc-07__bar">
<span class="bc-07__logo" aria-hidden="true"></span>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#" aria-current="page">Scroll-driven UI</a></li>
</ol>
</nav>
</header>
<div class="bc-07__sentinel" aria-hidden="true"></div>
<div class="bc-07__content">
<h1 class="bc-07__h1">Scroll-driven sticky breadcrumb header</h1>
<p class="bc-07__lede">Scroll this panel ↓ and watch the sticky breadcrumb header at the top compress into a slim pinned bar. Padding, type scale, and logo size all contract via CSS <code>animation-timeline: scroll()</code> — no JavaScript scroll listener needed.</p>
<h2 class="bc-07__h2">How the shrink is wired</h2>
<p>Modern browsers support scroll-driven animations natively (Chrome 115+, Safari 26+, Firefox 134+). The trick is one <code>@keyframes</code> block plus <code>animation-timeline: scroll(nearest)</code> on the sticky header. As the user scrolls past a sentinel div, the timeline advances from 0 to 1 and the browser interpolates the keyframe values.</p>
<p>This is the exact pattern <strong>Docusaurus 3</strong>, <strong>Mintlify</strong>, <strong>Nextra</strong>, <strong>VitePress</strong>, <strong>Starlight</strong>, and <strong>Fumadocs</strong> ship on their generated docs sites. No IntersectionObserver, no scroll event listener, no rAF throttle needed — the browser’s own compositor drives the animation.</p>
<h2 class="bc-07__h2">Accessibility contract</h2>
<p>The breadcrumb still needs to satisfy WCAG 2.4.8 (Location) and WCAG 1.4.3 (Contrast Minimum) regardless of shrink state. Both expanded and compact states use a token-based color system that maintains AA contrast against the frosted-glass backdrop.</p>
<p>Motion is gated behind <code>@media (prefers-reduced-motion: no-preference)</code> — users who opted out of motion at the OS level see a static bar at the compact size, never the animation. This is WCAG 2.3.3 (Animation from Interactions) compliance.</p>
<h2 class="bc-07__h2">Falling back gracefully</h2>
<p>Older Safari and Firefox versions that don’t support <code>animation-timeline: scroll()</code> just render the base state — a sticky bar with the expanded padding. The demo also ships a 12-line IntersectionObserver fallback that toggles a <code>.is-shrunk</code> class when the sentinel exits the viewport, so the shrink still works for the ~5% of users on older engines.</p>
<p>Feature-detect once at page load: <code>if (CSS.supports('animation-timeline', 'scroll()')) { /* skip JS fallback */ }</code>. Ship both paths; let the browser pick.</p>
<h2 class="bc-07__h2">Try it: scroll to the bottom</h2>
<p>The last paragraph sits at the far bottom so you can watch the header shrink smoothly as the sentinel scrolls out of view. Grab the scrollbar or two-finger swipe to see the full range of the animation.</p>
<p>Keep scrolling ↓</p>
<p>The shrink kicks in at the sentinel boundary, then holds at the compact size all the way to the bottom.</p>
<p>Once you scroll back up past the sentinel, the header expands smoothly back to its original size — the animation is bidirectional.</p>
<p>The frosted-glass <code>backdrop-filter</code> stays consistent throughout, so body text never fights the header for contrast.</p>
<p>This is the scroll-driven pattern behind every modern docs-site UX from 2024 onward.</p>
<p>End of scroll region.</p>
</div>
</div>
```
## CSS
```css
.bc-07 {
display: grid;
width: 100%;
min-height: 100vh;
font-family: system-ui,'Segoe UI',sans-serif;
height: 300px;
overflow-y: auto;
background: oklch(0.97 0.006 255);
position: relative;
scrollbar-width: thin;
place-items: center;
}
.bc-07 * {
box-sizing: border-box;
}
.bc-07__bar {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 14px;
padding: 20px 24px;
background: oklch(1 0 0/.85);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-bottom: 1px solid oklch(0.9 0.01 255);
box-shadow: 0 0 0 rgba(0,0,0,0);
transition: padding .3s ease,box-shadow .3s ease;
}
.bc-07__logo {
width: 34px;
height: 34px;
flex: none;
border-radius: 9px;
background: conic-gradient(from 210deg,oklch(0.62 0.17 265),oklch(0.6 0.16 210));
transition: width .3s ease,height .3s ease;
}
.bc-07 ol {
display: flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.bc-07 li {
display: flex;
align-items: center;
}
.bc-07 li + li::before {
content: "›";
margin: 0 8px;
color: oklch(0.72 0.02 255);
}
.bc-07 a {
font: 600 15px/1 system-ui,sans-serif;
color: oklch(0.5 0.1 260);
text-decoration: none;
padding: 3px 5px;
border-radius: 6px;
transition: font-size .3s ease,color .15s ease;
}
.bc-07 a:hover {
color: oklch(0.42 0.14 260);
}
.bc-07 a:focus-visible {
outline: 2px solid oklch(0.5 0.15 260);
outline-offset: 2px;
}
.bc-07 li[aria-current="page"] a {
color: oklch(0.28 0.02 255);
font-weight: 700;
}
.bc-07[data-shrunk="true"] .bc-07__bar {
padding: 9px 24px;
box-shadow: 0 4px 18px rgba(0,0,0,.1);
}
.bc-07[data-shrunk="true"] .bc-07__logo {
width: 24px;
height: 24px;
}
.bc-07[data-shrunk="true"] .bc-07 a {
font-size: 13px;
}
.bc-07__sentinel {
height: 1px;
}
@media (prefers-reduced-motion: reduce) {
.bc-07__bar,
.bc-07__logo,
.bc-07 a {
transition: none;
}
}
.bc-07 .bc-07__h1 {
font: 700 22px/1.3 system-ui,sans-serif;
color: oklch(0.25 0.02 255);
margin: 0 0 8px;
}
.bc-07 .bc-07__h2 {
font: 700 15px/1.3 system-ui,sans-serif;
color: oklch(0.3 0.02 255);
margin: 24px 0 6px;
}
.bc-07 .bc-07__lede {
font: 500 15px/1.55 system-ui,sans-serif;
color: oklch(0.42 0.02 255);
margin: 0 0 20px;
max-width: 60ch;
}
.bc-07 .bc-07__content {
padding: 24px 24px 40px;
}
.bc-07 .bc-07__content p {
margin: 0 0 14px;
color: oklch(0.42 0.02 255);
font-size: 14px;
line-height: 1.65;
max-width: 60ch;
}
.bc-07 .bc-07__content code {
font: 500 12.5px/1 ui-monospace,Menlo,monospace;
color: oklch(0.35 0.05 260);
background: oklch(0.94 0.01 255);
padding: 2px 6px;
border-radius: 4px;
}
.bc-07 .bc-07__content strong {
color: oklch(0.28 0.02 255);
font-weight: 700;
}
```
## JavaScript
```js
/* Scroll-shrink via IntersectionObserver (universal fallback for animation-timeline:scroll()) */
document.querySelectorAll('.bc-07').forEach((root) => {
const sentinel = root.querySelector('.bc-07__sentinel');
const io = new IntersectionObserver(
([e]) => root.setAttribute('data-shrunk', String(!e.isIntersecting)),
{ root, threshold: 0 }
);
io.observe(sentinel);
});
/* Pure-CSS alternative: give .bc-07__bar { animation: shrink linear both; animation-timeline: scroll(nearest); } and keyframe the same properties. */
```Here's a working CSS Breadcrumb from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Sticky Top CSS Navigation Header with Shrinking Breadcrumb Trail
Source: https://codefronts.com/navigation/css-breadcrumbs/sticky-top-css-navigation-header-with-shrinking-breadcrumb-trail/
A sticky page header whose breadcrumb trail shrinks as you scroll — font size, padding and vertical footprint all contract into a compact bar that stays pinned to the top. Modern build uses CSS scroll-driven animation-timeline:scroll(); a tiny IntersectionObserver fallback ships for engines without it.
## HTML
```html
<div class="bc-07" data-shrunk="false">
<header class="bc-07__bar">
<span class="bc-07__logo" aria-hidden="true"></span>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li><a href="#">Docs</a></li>
<li><a href="#" aria-current="page">Scroll-driven UI</a></li>
</ol>
</nav>
</header>
<div class="bc-07__sentinel" aria-hidden="true"></div>
<div class="bc-07__content">
<h1 class="bc-07__h1">Scroll-driven sticky breadcrumb header</h1>
<p class="bc-07__lede">Scroll this panel ↓ and watch the sticky breadcrumb header at the top compress into a slim pinned bar. Padding, type scale, and logo size all contract via CSS <code>animation-timeline: scroll()</code> — no JavaScript scroll listener needed.</p>
<h2 class="bc-07__h2">How the shrink is wired</h2>
<p>Modern browsers support scroll-driven animations natively (Chrome 115+, Safari 26+, Firefox 134+). The trick is one <code>@keyframes</code> block plus <code>animation-timeline: scroll(nearest)</code> on the sticky header. As the user scrolls past a sentinel div, the timeline advances from 0 to 1 and the browser interpolates the keyframe values.</p>
<p>This is the exact pattern <strong>Docusaurus 3</strong>, <strong>Mintlify</strong>, <strong>Nextra</strong>, <strong>VitePress</strong>, <strong>Starlight</strong>, and <strong>Fumadocs</strong> ship on their generated docs sites. No IntersectionObserver, no scroll event listener, no rAF throttle needed — the browser’s own compositor drives the animation.</p>
<h2 class="bc-07__h2">Accessibility contract</h2>
<p>The breadcrumb still needs to satisfy WCAG 2.4.8 (Location) and WCAG 1.4.3 (Contrast Minimum) regardless of shrink state. Both expanded and compact states use a token-based color system that maintains AA contrast against the frosted-glass backdrop.</p>
<p>Motion is gated behind <code>@media (prefers-reduced-motion: no-preference)</code> — users who opted out of motion at the OS level see a static bar at the compact size, never the animation. This is WCAG 2.3.3 (Animation from Interactions) compliance.</p>
<h2 class="bc-07__h2">Falling back gracefully</h2>
<p>Older Safari and Firefox versions that don’t support <code>animation-timeline: scroll()</code> just render the base state — a sticky bar with the expanded padding. The demo also ships a 12-line IntersectionObserver fallback that toggles a <code>.is-shrunk</code> class when the sentinel exits the viewport, so the shrink still works for the ~5% of users on older engines.</p>
<p>Feature-detect once at page load: <code>if (CSS.supports('animation-timeline', 'scroll()')) { /* skip JS fallback */ }</code>. Ship both paths; let the browser pick.</p>
<h2 class="bc-07__h2">Try it: scroll to the bottom</h2>
<p>The last paragraph sits at the far bottom so you can watch the header shrink smoothly as the sentinel scrolls out of view. Grab the scrollbar or two-finger swipe to see the full range of the animation.</p>
<p>Keep scrolling ↓</p>
<p>The shrink kicks in at the sentinel boundary, then holds at the compact size all the way to the bottom.</p>
<p>Once you scroll back up past the sentinel, the header expands smoothly back to its original size — the animation is bidirectional.</p>
<p>The frosted-glass <code>backdrop-filter</code> stays consistent throughout, so body text never fights the header for contrast.</p>
<p>This is the scroll-driven pattern behind every modern docs-site UX from 2024 onward.</p>
<p>End of scroll region.</p>
</div>
</div>
```
## CSS
```css
.bc-07 {
display: grid;
width: 100%;
min-height: 100vh;
font-family: system-ui,'Segoe UI',sans-serif;
height: 300px;
overflow-y: auto;
background: oklch(0.97 0.006 255);
position: relative;
scrollbar-width: thin;
place-items: center;
}
.bc-07 * {
box-sizing: border-box;
}
.bc-07__bar {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
gap: 14px;
padding: 20px 24px;
background: oklch(1 0 0/.85);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-bottom: 1px solid oklch(0.9 0.01 255);
box-shadow: 0 0 0 rgba(0,0,0,0);
transition: padding .3s ease,box-shadow .3s ease;
}
.bc-07__logo {
width: 34px;
height: 34px;
flex: none;
border-radius: 9px;
background: conic-gradient(from 210deg,oklch(0.62 0.17 265),oklch(0.6 0.16 210));
transition: width .3s ease,height .3s ease;
}
.bc-07 ol {
display: flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
}
.bc-07 li {
display: flex;
align-items: center;
}
.bc-07 li + li::before {
content: "›";
margin: 0 8px;
color: oklch(0.72 0.02 255);
}
.bc-07 a {
font: 600 15px/1 system-ui,sans-serif;
color: oklch(0.5 0.1 260);
text-decoration: none;
padding: 3px 5px;
border-radius: 6px;
transition: font-size .3s ease,color .15s ease;
}
.bc-07 a:hover {
color: oklch(0.42 0.14 260);
}
.bc-07 a:focus-visible {
outline: 2px solid oklch(0.5 0.15 260);
outline-offset: 2px;
}
.bc-07 li[aria-current="page"] a {
color: oklch(0.28 0.02 255);
font-weight: 700;
}
.bc-07[data-shrunk="true"] .bc-07__bar {
padding: 9px 24px;
box-shadow: 0 4px 18px rgba(0,0,0,.1);
}
.bc-07[data-shrunk="true"] .bc-07__logo {
width: 24px;
height: 24px;
}
.bc-07[data-shrunk="true"] .bc-07 a {
font-size: 13px;
}
.bc-07__sentinel {
height: 1px;
}
@media (prefers-reduced-motion: reduce) {
.bc-07__bar,
.bc-07__logo,
.bc-07 a {
transition: none;
}
}
.bc-07 .bc-07__h1 {
font: 700 22px/1.3 system-ui,sans-serif;
color: oklch(0.25 0.02 255);
margin: 0 0 8px;
}
.bc-07 .bc-07__h2 {
font: 700 15px/1.3 system-ui,sans-serif;
color: oklch(0.3 0.02 255);
margin: 24px 0 6px;
}
.bc-07 .bc-07__lede {
font: 500 15px/1.55 system-ui,sans-serif;
color: oklch(0.42 0.02 255);
margin: 0 0 20px;
max-width: 60ch;
}
.bc-07 .bc-07__content {
padding: 24px 24px 40px;
}
.bc-07 .bc-07__content p {
margin: 0 0 14px;
color: oklch(0.42 0.02 255);
font-size: 14px;
line-height: 1.65;
max-width: 60ch;
}
.bc-07 .bc-07__content code {
font: 500 12.5px/1 ui-monospace,Menlo,monospace;
color: oklch(0.35 0.05 260);
background: oklch(0.94 0.01 255);
padding: 2px 6px;
border-radius: 4px;
}
.bc-07 .bc-07__content strong {
color: oklch(0.28 0.02 255);
font-weight: 700;
}
```
## JavaScript
```js
/* Scroll-shrink via IntersectionObserver (universal fallback for animation-timeline:scroll()) */
document.querySelectorAll('.bc-07').forEach((root) => {
const sentinel = root.querySelector('.bc-07__sentinel');
const io = new IntersectionObserver(
([e]) => root.setAttribute('data-shrunk', String(!e.isIntersecting)),
{ root, threshold: 0 }
);
io.observe(sentinel);
});
/* Pure-CSS alternative: give .bc-07__bar { animation: shrink linear both; animation-timeline: scroll(nearest); } and keyframe the same properties. */
```How this works
The header is position:sticky;top:0;z-index:100 so it pins as the page scrolls. The shrink is driven by a scroll progress signal: the modern path is a CSS animation-timeline:scroll(root) that ties a keyframed --shrink (0→1) to the document's scroll position, and the header interpolates padding, font-size and logo scale from that variable — zero JS. Because some engines and some embedded contexts (like a gallery tile that scrolls internally) don't expose a usable scroll timeline, the demo ships a 12-line IntersectionObserver that toggles a data-shrunk attribute when a sentinel scrolls out of view — a robust, universally-supported fallback that drives the exact same CSS transitions.
Either way the visual contract is identical: a set of properties transition between a 'tall' and 'compact' state. Keeping the trail readable while compact matters — the current page label never shrinks below 13px, and the whole bar keeps its accessible <nav> semantics throughout. The transition is guarded by prefers-reduced-motion, which pins the compact state immediately instead of animating on every scroll tick.
Make it yours
- Prefer pure CSS? Delete the JS and use
animation-timeline:scroll()+@keyframesdriving--shrink— supported in Chrome 115+. - Shrink threshold is the sentinel height (or the scroll-timeline range) — 80px feels natural; raise it to delay the collapse.
- Add a subtle
box-shadowthat fades in only when shrunk to lift the pinned bar off the content. - Hide intermediate crumbs when compact (combine with demo 03's collapse) for an ultra-dense scrolled state.
Gotchas — read before shipping
position:stickysilently fails if any ancestor hasoverflow:hidden/auto— the demo's stage is the scroll container, so the sticky is relative to it.- Scroll-driven
animation-timelineneeds a scroll container with actual overflow; in a non-scrolling embed it never advances — that's exactly why the JS fallback exists. - Animate
padding/font-sizetransitions, not layout-thrashing properties in a scroll handler — set a class/attr and let CSS transition, as here. - Debounce is unnecessary with IntersectionObserver (it fires only on threshold crossing) — don't reintroduce a scroll listener.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ (CSS) / all (JS fallback) | all (JS fallback) | all (JS fallback) | 115+ (CSS) / all (JS fallback) |
animation-timeline:scroll() is Chrome 115+/Firefox 111+; the IntersectionObserver fallback (shipped here) runs everywhere back to 2019.