<div><span class="lbl">scroll-state(stuck): headers that change once they stick</span>
<div class="mdn mdn-ss">
<p class="mdn-note">Chrome and Edge 133+. Elsewhere the headers simply stick. <span class="mdn-yes">Works in this browser</span><span class="mdn-no">Fallback in this browser</span></p>
<div class="mdn-box" tabindex="0" role="region" aria-label="Island ferry timetable, scrollable">
<div class="mdn-day"><h3>Monday<span>4 sailings</span></h3></div>
<ul class="mdn-times" aria-label="Monday sailings"><li><b>06:40</b><span>Oban to Craignure</span></li><li><b>09:15</b><span>Craignure to Oban</span></li><li><b>13:30</b><span>Oban to Craignure</span></li><li><b>17:05</b><span>Craignure to Oban</span></li></ul>
<div class="mdn-day"><h3>Tuesday<span>3 sailings</span></h3></div>
<ul class="mdn-times" aria-label="Tuesday sailings"><li><b>07:10</b><span>Oban to Craignure</span></li><li><b>12:00</b><span>Craignure to Oban</span></li><li><b>18:20</b><span>Oban to Craignure</span></li></ul>
<div class="mdn-day"><h3>Wednesday<span>4 sailings</span></h3></div>
<ul class="mdn-times" aria-label="Wednesday sailings"><li><b>06:40</b><span>Oban to Craignure</span></li><li><b>10:45</b><span>Craignure to Oban</span></li><li><b>14:30</b><span>Oban to Craignure</span></li><li><b>19:00</b><span>Craignure to Oban</span></li></ul>
</div>
</div></div>
<div><span class="lbl">scroll-state(scrollable): shadows only while there is more</span>
<div class="mdn mdn-ss">
<p class="mdn-note">Chrome and Edge 133+. Elsewhere the list scrolls without the shadows. <span class="mdn-yes">Works in this browser</span><span class="mdn-no">Fallback in this browser</span></p>
<div class="mdn-box mdn-shadowbox" tabindex="0" role="region" aria-label="Release notes, scrollable">
<i class="mdn-shade top" aria-hidden="true"></i>
<ul class="mdn-notes">
<li><b>4.2.0 · 14 March</b>Offline mode for the timetable, with the next seven days saved on the phone.</li>
<li><b>4.1.3 · 2 March</b>Fixed a crash when a sailing was cancelled while the page was open.</li>
<li><b>4.1.2 · 20 February</b>Times now follow the phone's 12 or 24 hour setting.</li>
<li><b>4.1.0 · 6 February</b>Live vessel positions on the map, updated every 30 seconds.</li>
<li><b>4.0.1 · 22 January</b>Bigger tap targets on the booking screen.</li>
<li><b>4.0.0 · 9 January</b>A new design with dark mode and larger text options.</li>
<li><b>3.9.4 · 12 December</b>Winter timetable added for all eleven routes.</li>
</ul>
<i class="mdn-shade bottom" aria-hidden="true"></i>
</div>
</div></div>
<div><span class="lbl">scroll-state(snapped): the card that is snapped</span>
<div class="mdn mdn-ss">
<p class="mdn-note">Chrome and Edge 133+. Elsewhere it is a normal snap carousel. <span class="mdn-yes">Works in this browser</span><span class="mdn-no">Fallback in this browser</span></p>
<div class="mdn-snap" tabindex="0" role="region" aria-label="Theme presets, scrolls sideways"><ul>
<li><div class="mdn-swatch"><i aria-hidden="true"><b style="background:#1e1b4b"></b><b style="background:#4f46e5"></b><b style="background:#a5b4fc"></b><b style="background:#eef2ff"></b></i><h3>Dusk</h3><p>Indigo with a pale lilac ground</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch"><i aria-hidden="true"><b style="background:#14532d"></b><b style="background:#16a34a"></b><b style="background:#bbf7d0"></b><b style="background:#f0fdf4"></b></i><h3>Moss</h3><p>Deep greens for calm dashboards</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch"><i aria-hidden="true"><b style="background:#7c2d12"></b><b style="background:#ea580c"></b><b style="background:#fed7aa"></b><b style="background:#fff7ed"></b></i><h3>Ember</h3><p>Warm orange with a cream ground</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch"><i aria-hidden="true"><b style="background:#831843"></b><b style="background:#db2777"></b><b style="background:#fbcfe8"></b><b style="background:#fdf2f8"></b></i><h3>Coral</h3><p>Pinks that work on light pages</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch"><i aria-hidden="true"><b style="background:#0c4a6e"></b><b style="background:#0284c7"></b><b style="background:#bae6fd"></b><b style="background:#f0f9ff"></b></i><h3>Harbour</h3><p>Sea blues with a white ground</p><small>Snapped</small></div></li>
</ul></div>
</div></div>
<div><span class="lbl">view-timeline ranges: one animation, four ranges</span>
<div class="mdn mdn-vt">
<p class="mdn-note">Chrome 115+, Safari 26+. Elsewhere the bars are drawn full. <span class="mdn-yes">Works in this browser</span><span class="mdn-no">Fallback in this browser</span></p>
<ul class="mdn-ranges">
<li><code>entry</code><p>Fills while the row slides in from the bottom edge.</p><i class="mdn-bar entry" aria-hidden="true"></i></li>
<li><code>contain</code><p>Fills while the whole row is on screen.</p><i class="mdn-bar contain" aria-hidden="true"></i></li>
<li><code>cover</code><p>Fills from first pixel in to last pixel out.</p><i class="mdn-bar cover" aria-hidden="true"></i></li>
<li><code>exit</code><p>Fills while the row leaves at the top edge.</p><i class="mdn-bar exit" aria-hidden="true"></i></li>
</ul>
</div></div>
<div><span class="lbl">timeline-scope: a scroll spy with no JavaScript</span>
<div class="mdn mdn-ts">
<p class="mdn-note">Chrome 116+, Safari 26+. Elsewhere the links still jump to each step, without the highlight. <span class="mdn-yes">Works in this browser</span><span class="mdn-no">Fallback in this browser</span></p>
<div class="mdn-spy">
<nav aria-label="Sourdough steps"><ol><li><a href="#mdn-starter">1. Starter</a></li><li><a href="#mdn-mix">2. Mix</a></li><li><a href="#mdn-proof">3. Proof</a></li><li><a href="#mdn-bake">4. Bake</a></li></ol></nav>
<div class="mdn-box" tabindex="0" role="region" aria-label="Sourdough method, scrollable">
<section id="mdn-starter" aria-labelledby="mdn-starter-h"><h3 id="mdn-starter-h">1. Feed the starter</h3><p>The night before, mix 50 g of starter with 50 g of flour and 50 g of water. By morning it should have doubled and smell sweet and sour.</p><p>Drop a spoonful in water: if it floats, it is ready.</p></section>
<section id="mdn-mix" aria-labelledby="mdn-mix-h"><h3 id="mdn-mix-h">2. Mix and fold</h3><p>Stir 100 g of starter into 350 g of water, add 500 g of strong flour and 10 g of salt, and mix until no dry flour is left.</p><p>Over the next two hours, stretch and fold the dough four times.</p></section>
<section id="mdn-proof" aria-labelledby="mdn-proof-h"><h3 id="mdn-proof-h">3. Shape and proof</h3><p>Shape a tight round, put it seam side up in a floured basket, cover it and leave it in the fridge for 12 to 16 hours.</p><p>Cold proofing deepens the flavour and makes scoring easier.</p></section>
<section id="mdn-bake" aria-labelledby="mdn-bake-h"><h3 id="mdn-bake-h">4. Bake</h3><p>Heat a lidded pot at 250 °C for 45 minutes. Tip the dough in, score it, bake 20 minutes with the lid on and 25 with it off.</p><p>Let it cool for an hour before cutting.</p></section>
</div>
</div>
</div></div>
CSS
/* Modern CSS scroll effects with no JavaScript, each with a stated baseline and a fallback.
- scroll-state() container queries (Chrome and Edge 133+): style a sticky header once it is stuck, show edge
shadows only while there is more to scroll, and mark the snapped card. Elsewhere: plain sticky, no shadows,
no snapped highlight.
- view-timeline ranges (Chrome 115+, Safari 26+): the same animation run over entry, contain and exit.
Elsewhere: the bars are drawn full.
- timeline-scope (Chrome 116+, Safari 26+): a scroll spy whose links sit outside the scroller they follow.
Elsewhere: the links still jump to each section, without the highlight.
Under prefers-reduced-motion the range bars and transitions stand still; state changes stay instant. */
.mdn{--mdn-ink:#14162b;--mdn-muted:#545873;--mdn-line:#dfe1ec;--mdn-card:#fff;--mdn-accent:#4f46e5;--mdn-accent-soft:#eef0ff;--mdn-pink:#be185d;
color:var(--mdn-ink)}
.mdn :where(h2,h3,p,ul,ol,dl,dd){margin:0}
.mdn-note{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-bottom:14px;font-size:.84rem;color:var(--mdn-muted)}
.mdn-yes,.mdn-no{padding:2px 10px;border-radius:999px;font-size:.76rem;font-weight:700}
.mdn-yes{display:none;background:#dcfce7;color:#14532d}
.mdn-no{background:#fef3c7;color:#713f12}
.mdn-box{height:320px;overflow-y:auto;border:1px solid var(--mdn-line);border-radius:18px;background:var(--mdn-card);overscroll-behavior:contain}
.mdn-box:focus-visible,.mdn-snap:focus-visible{outline:3px solid var(--mdn-accent);outline-offset:3px}
/* 1. stuck headers */
.mdn-day{position:sticky;top:0;z-index:1;container-type:scroll-state}
.mdn-day h3{display:flex;align-items:center;gap:8px;padding:12px 20px;background:var(--mdn-card);border-bottom:1px solid var(--mdn-line);font-size:.95rem;
transition:padding .2s,background-color .2s,box-shadow .2s,color .2s}
.mdn-day h3 span{margin-left:auto;font-size:.78rem;font-weight:600;color:var(--mdn-muted)}
.mdn-times{list-style:none;padding:6px 20px 14px}
.mdn-times li{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px dashed var(--mdn-line);font-size:.92rem}
.mdn-times b{font-variant-numeric:tabular-nums}
.mdn-times span{color:var(--mdn-muted)}
@container scroll-state(stuck: top){
.mdn-day h3{padding-block:8px;background:var(--mdn-ink);color:#fff;box-shadow:0 10px 20px -12px rgba(20,22,43,.6)}
.mdn-day h3 span{color:#c7cbe8}
}
@supports (container-type:scroll-state){.mdn-ss .mdn-yes{display:inline}.mdn-ss .mdn-no{display:none}}
/* 2. scroll shadows that appear only while there is more to scroll */
.mdn-shadowbox{container-type:scroll-state}
.mdn-shade{position:sticky;z-index:1;display:block;height:0;pointer-events:none}
.mdn-shade::after{content:"";position:absolute;left:0;right:0;height:22px;opacity:0;transition:opacity .2s}
.mdn-shade.top{top:0}
.mdn-shade.top::after{top:0;background:linear-gradient(rgba(20,22,43,.22),transparent)}
.mdn-shade.bottom{bottom:0}
.mdn-shade.bottom::after{bottom:0;background:linear-gradient(transparent,rgba(20,22,43,.22))}
@container scroll-state(scrollable: top){.mdn-shade.top::after{opacity:1}}
@container scroll-state(scrollable: bottom){.mdn-shade.bottom::after{opacity:1}}
.mdn-notes{list-style:none;padding:8px 20px}
.mdn-notes li{padding:12px 0;border-bottom:1px solid var(--mdn-line);font-size:.92rem;color:var(--mdn-muted)}
.mdn-notes b{display:block;color:var(--mdn-ink)}
/* 3. snapped card */
.mdn-snap{padding:8px 4px 16px;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:4px;border-radius:18px}
.mdn-snap ul{list-style:none;padding:0;display:flex;gap:14px;width:max-content}
.mdn-snap li{flex:none;width:clamp(200px,32vw,260px);scroll-snap-align:start;container-type:scroll-state}
.mdn-swatch{padding:14px;border:2px solid var(--mdn-line);border-radius:18px;background:var(--mdn-card);transition:border-color .2s,transform .2s,box-shadow .2s}
.mdn-swatch i{display:flex;height:96px;border-radius:12px;overflow:hidden}
.mdn-swatch i b{flex:1}
.mdn-swatch h3{margin-top:12px;font-size:1rem}
.mdn-swatch p{font-size:.85rem;color:var(--mdn-muted)}
.mdn-swatch small{display:none;margin-top:6px;font-size:.75rem;font-weight:700;color:var(--mdn-accent)}
@container scroll-state(snapped: x){
.mdn-swatch{border-color:var(--mdn-accent);box-shadow:0 14px 30px -18px rgba(79,70,229,.6)}
.mdn-swatch small{display:block}
}
/* 4. view-timeline ranges */
.mdn-ranges{list-style:none;padding:0;display:grid;gap:12px}
.mdn-ranges li{display:grid;grid-template-columns:minmax(120px,170px) 1fr;align-items:center;gap:16px;padding:16px 20px;border:1px solid var(--mdn-line);border-radius:16px;background:var(--mdn-card)}
.mdn-ranges code{font:600 .9rem ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--mdn-pink)}
.mdn-ranges p{font-size:.86rem;color:var(--mdn-muted)}
.mdn-bar{grid-column:1/-1;height:8px;border-radius:4px;background:var(--mdn-accent-soft);overflow:hidden}
.mdn-bar::before{content:"";display:block;height:100%;background:linear-gradient(90deg,var(--mdn-accent),var(--mdn-pink));transform-origin:0 50%}
@keyframes mdn-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@supports (animation-range:entry){
@media (prefers-reduced-motion:no-preference){
.mdn-ranges li{view-timeline:--mdn-row block}
.mdn-bar::before{animation:mdn-fill linear both;animation-timeline:--mdn-row}
.mdn-bar.entry::before{animation-range:entry 0% entry 100%}
.mdn-bar.contain::before{animation-range:contain 0% contain 100%}
.mdn-bar.cover::before{animation-range:cover 0% cover 100%}
.mdn-bar.exit::before{animation-range:exit 0% exit 100%}
}
.mdn-vt .mdn-yes{display:inline}.mdn-vt .mdn-no{display:none}
}
/* 5. timeline-scope scroll spy */
.mdn-spy{display:grid;grid-template-columns:180px 1fr;gap:16px;timeline-scope:--mdn-s1,--mdn-s2,--mdn-s3,--mdn-s4}
.mdn-spy nav ol{list-style:none;padding:0;display:grid;gap:6px;position:sticky;top:12px}
.mdn-spy nav a{display:block;padding:9px 14px;border-radius:10px;color:var(--mdn-ink);font-size:.92rem;font-weight:600;text-decoration:none;border:1px solid var(--mdn-line);background:var(--mdn-card)}
.mdn-spy nav a:hover{border-color:var(--mdn-accent)}
.mdn-spy nav a:focus-visible{outline:3px solid var(--mdn-accent);outline-offset:2px}
.mdn-spy .mdn-box section{min-height:100%;padding:22px 24px;border-bottom:1px solid var(--mdn-line)}
.mdn-spy h3{font-size:1.1rem}
.mdn-spy section p{margin-top:8px;max-width:56ch;color:var(--mdn-muted)}
.mdn-spy section:nth-child(1){view-timeline:--mdn-s1 block}
.mdn-spy section:nth-child(2){view-timeline:--mdn-s2 block}
.mdn-spy section:nth-child(3){view-timeline:--mdn-s3 block}
.mdn-spy section:nth-child(4){view-timeline:--mdn-s4 block}
@keyframes mdn-on{from,to{background:var(--mdn-ink);color:#fff;border-color:var(--mdn-ink)}}
@supports (timeline-scope:--a){
/* the link is highlighted while its section crosses the middle of the box (sections are one box tall) */
.mdn-spy nav a{animation:mdn-on linear;animation-range:cover 25% cover 75%}
.mdn-spy nav li:nth-child(1) a{animation-timeline:--mdn-s1}
.mdn-spy nav li:nth-child(2) a{animation-timeline:--mdn-s2}
.mdn-spy nav li:nth-child(3) a{animation-timeline:--mdn-s3}
.mdn-spy nav li:nth-child(4) a{animation-timeline:--mdn-s4}
.mdn-ts .mdn-yes{display:inline}.mdn-ts .mdn-no{display:none}
}
@media (max-width:640px){
.mdn-spy{grid-template-columns:1fr}
.mdn-spy nav ol{grid-template-columns:repeat(2,1fr);position:static}
.mdn-ranges li{grid-template-columns:1fr;gap:6px}
}
@media (prefers-reduced-motion:reduce){
.mdn *,.mdn *::before,.mdn *::after{transition:none!important}
}
Bootstrap 5 code
Requires: Bootstrap 5.3.8 CSS
HTML
<div><span class="lbl">scroll-state(stuck): headers that change once they stick</span>
<div class="mdn mdn-ss">
<p class="mdn-note">Chrome and Edge 133+. Elsewhere the headers simply stick. <span class="mdn-yes badge rounded-pill">Works in this browser</span><span class="mdn-no badge rounded-pill">Fallback in this browser</span></p>
<div class="mdn-box border rounded-4" tabindex="0" role="region" aria-label="Island ferry timetable, scrollable">
<div class="mdn-day"><h3>Monday<span>4 sailings</span></h3></div>
<ul class="mdn-times" aria-label="Monday sailings"><li><b>06:40</b><span>Oban to Craignure</span></li><li><b>09:15</b><span>Craignure to Oban</span></li><li><b>13:30</b><span>Oban to Craignure</span></li><li><b>17:05</b><span>Craignure to Oban</span></li></ul>
<div class="mdn-day"><h3>Tuesday<span>3 sailings</span></h3></div>
<ul class="mdn-times" aria-label="Tuesday sailings"><li><b>07:10</b><span>Oban to Craignure</span></li><li><b>12:00</b><span>Craignure to Oban</span></li><li><b>18:20</b><span>Oban to Craignure</span></li></ul>
<div class="mdn-day"><h3>Wednesday<span>4 sailings</span></h3></div>
<ul class="mdn-times" aria-label="Wednesday sailings"><li><b>06:40</b><span>Oban to Craignure</span></li><li><b>10:45</b><span>Craignure to Oban</span></li><li><b>14:30</b><span>Oban to Craignure</span></li><li><b>19:00</b><span>Craignure to Oban</span></li></ul>
</div>
</div></div>
<div><span class="lbl">scroll-state(scrollable): shadows only while there is more</span>
<div class="mdn mdn-ss">
<p class="mdn-note">Chrome and Edge 133+. Elsewhere the list scrolls without the shadows. <span class="mdn-yes badge rounded-pill">Works in this browser</span><span class="mdn-no badge rounded-pill">Fallback in this browser</span></p>
<div class="mdn-box mdn-shadowbox border rounded-4" tabindex="0" role="region" aria-label="Release notes, scrollable">
<i class="mdn-shade top" aria-hidden="true"></i>
<ul class="mdn-notes list-group list-group-flush">
<li class="list-group-item"><b>4.2.0 · 14 March</b>Offline mode for the timetable, with the next seven days saved on the phone.</li>
<li class="list-group-item"><b>4.1.3 · 2 March</b>Fixed a crash when a sailing was cancelled while the page was open.</li>
<li class="list-group-item"><b>4.1.2 · 20 February</b>Times now follow the phone's 12 or 24 hour setting.</li>
<li class="list-group-item"><b>4.1.0 · 6 February</b>Live vessel positions on the map, updated every 30 seconds.</li>
<li class="list-group-item"><b>4.0.1 · 22 January</b>Bigger tap targets on the booking screen.</li>
<li class="list-group-item"><b>4.0.0 · 9 January</b>A new design with dark mode and larger text options.</li>
<li class="list-group-item"><b>3.9.4 · 12 December</b>Winter timetable added for all eleven routes.</li>
</ul>
<i class="mdn-shade bottom" aria-hidden="true"></i>
</div>
</div></div>
<div><span class="lbl">scroll-state(snapped): the card that is snapped</span>
<div class="mdn mdn-ss">
<p class="mdn-note">Chrome and Edge 133+. Elsewhere it is a normal snap carousel. <span class="mdn-yes badge rounded-pill">Works in this browser</span><span class="mdn-no badge rounded-pill">Fallback in this browser</span></p>
<div class="mdn-snap" tabindex="0" role="region" aria-label="Theme presets, scrolls sideways"><ul>
<li><div class="mdn-swatch card"><i aria-hidden="true"><b style="background:#1e1b4b"></b><b style="background:#4f46e5"></b><b style="background:#a5b4fc"></b><b style="background:#eef2ff"></b></i><h3>Dusk</h3><p>Indigo with a pale lilac ground</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch card"><i aria-hidden="true"><b style="background:#14532d"></b><b style="background:#16a34a"></b><b style="background:#bbf7d0"></b><b style="background:#f0fdf4"></b></i><h3>Moss</h3><p>Deep greens for calm dashboards</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch card"><i aria-hidden="true"><b style="background:#7c2d12"></b><b style="background:#ea580c"></b><b style="background:#fed7aa"></b><b style="background:#fff7ed"></b></i><h3>Ember</h3><p>Warm orange with a cream ground</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch card"><i aria-hidden="true"><b style="background:#831843"></b><b style="background:#db2777"></b><b style="background:#fbcfe8"></b><b style="background:#fdf2f8"></b></i><h3>Coral</h3><p>Pinks that work on light pages</p><small>Snapped</small></div></li>
<li><div class="mdn-swatch card"><i aria-hidden="true"><b style="background:#0c4a6e"></b><b style="background:#0284c7"></b><b style="background:#bae6fd"></b><b style="background:#f0f9ff"></b></i><h3>Harbour</h3><p>Sea blues with a white ground</p><small>Snapped</small></div></li>
</ul></div>
</div></div>
<div><span class="lbl">view-timeline ranges: one animation, four ranges</span>
<div class="mdn mdn-vt">
<p class="mdn-note">Chrome 115+, Safari 26+. Elsewhere the bars are drawn full. <span class="mdn-yes badge rounded-pill">Works in this browser</span><span class="mdn-no badge rounded-pill">Fallback in this browser</span></p>
<ul class="mdn-ranges list-unstyled mb-0">
<li class="card"><code>entry</code><p>Fills while the row slides in from the bottom edge.</p><i class="mdn-bar entry" aria-hidden="true"></i></li>
<li class="card"><code>contain</code><p>Fills while the whole row is on screen.</p><i class="mdn-bar contain" aria-hidden="true"></i></li>
<li class="card"><code>cover</code><p>Fills from first pixel in to last pixel out.</p><i class="mdn-bar cover" aria-hidden="true"></i></li>
<li class="card"><code>exit</code><p>Fills while the row leaves at the top edge.</p><i class="mdn-bar exit" aria-hidden="true"></i></li>
</ul>
</div></div>
<div><span class="lbl">timeline-scope: a scroll spy with no JavaScript</span>
<div class="mdn mdn-ts">
<p class="mdn-note">Chrome 116+, Safari 26+. Elsewhere the links still jump to each step, without the highlight. <span class="mdn-yes badge rounded-pill">Works in this browser</span><span class="mdn-no badge rounded-pill">Fallback in this browser</span></p>
<div class="mdn-spy">
<nav aria-label="Sourdough steps"><ol class="nav nav-pills flex-column"><li class="nav-item"><a class="nav-link" href="#mdn-starter">1. Starter</a></li><li class="nav-item"><a class="nav-link" href="#mdn-mix">2. Mix</a></li><li class="nav-item"><a class="nav-link" href="#mdn-proof">3. Proof</a></li><li class="nav-item"><a class="nav-link" href="#mdn-bake">4. Bake</a></li></ol></nav>
<div class="mdn-box border rounded-4" tabindex="0" role="region" aria-label="Sourdough method, scrollable">
<section id="mdn-starter" aria-labelledby="mdn-starter-h"><h3 id="mdn-starter-h">1. Feed the starter</h3><p>The night before, mix 50 g of starter with 50 g of flour and 50 g of water. By morning it should have doubled and smell sweet and sour.</p><p>Drop a spoonful in water: if it floats, it is ready.</p></section>
<section id="mdn-mix" aria-labelledby="mdn-mix-h"><h3 id="mdn-mix-h">2. Mix and fold</h3><p>Stir 100 g of starter into 350 g of water, add 500 g of strong flour and 10 g of salt, and mix until no dry flour is left.</p><p>Over the next two hours, stretch and fold the dough four times.</p></section>
<section id="mdn-proof" aria-labelledby="mdn-proof-h"><h3 id="mdn-proof-h">3. Shape and proof</h3><p>Shape a tight round, put it seam side up in a floured basket, cover it and leave it in the fridge for 12 to 16 hours.</p><p>Cold proofing deepens the flavour and makes scoring easier.</p></section>
<section id="mdn-bake" aria-labelledby="mdn-bake-h"><h3 id="mdn-bake-h">4. Bake</h3><p>Heat a lidded pot at 250 °C for 45 minutes. Tip the dough in, score it, bake 20 minutes with the lid on and 25 with it off.</p><p>Let it cool for an hour before cutting.</p></section>
</div>
</div>
</div></div>
CSS
/* Modern CSS scroll effects in Bootstrap 5: .badge, .card, .list-group, .nav-pills and .border/.rounded-4 for the
parts, and the shared CSS below for the scroll behaviour. No JavaScript, each with a stated baseline and a fallback.
- scroll-state() container queries (Chrome and Edge 133+): style a sticky header once it is stuck, show edge
shadows only while there is more to scroll, and mark the snapped card. Elsewhere: plain sticky, no shadows,
no snapped highlight.
- view-timeline ranges (Chrome 115+, Safari 26+): the same animation run over entry, contain and exit.
Elsewhere: the bars are drawn full.
- timeline-scope (Chrome 116+, Safari 26+): a scroll spy whose links sit outside the scroller they follow.
Elsewhere: the links still jump to each section, without the highlight.
Under prefers-reduced-motion the range bars and transitions stand still; state changes stay instant. */
.mdn{--mdn-ink:#14162b;--mdn-muted:#545873;--mdn-line:#dfe1ec;--mdn-card:#fff;--mdn-accent:#4f46e5;--mdn-accent-soft:#eef0ff;--mdn-pink:#be185d;
color:var(--mdn-ink)}
.mdn :where(h2,h3,p,ul,ol,dl,dd){margin:0}
.mdn-note{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-bottom:14px;font-size:.84rem;color:var(--mdn-muted)}
.mdn-yes,.mdn-no{--bs-badge-font-size:.76rem;--bs-badge-padding-x:10px;--bs-badge-padding-y:4px}
.mdn-yes{display:none;background:#dcfce7!important;color:#14532d}
.mdn-no{background:#fef3c7;color:#713f12}
.mdn-box{height:320px;overflow-y:auto;border-color:var(--mdn-line)!important;background:var(--mdn-card);overscroll-behavior:contain}
.mdn-box:focus-visible,.mdn-snap:focus-visible{outline:3px solid var(--mdn-accent);outline-offset:3px}
/* 1. stuck headers */
.mdn-day{position:sticky;top:0;z-index:1;container-type:scroll-state}
.mdn-day h3{display:flex;align-items:center;gap:8px;padding:12px 20px;background:var(--mdn-card);border-bottom:1px solid var(--mdn-line);font-size:.95rem;font-weight:700;
transition:padding .2s,background-color .2s,box-shadow .2s,color .2s}
.mdn-day h3 span{margin-left:auto;font-size:.78rem;font-weight:600;color:var(--mdn-muted)}
.mdn-times{list-style:none;padding:6px 20px 14px}
.mdn-times li{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px dashed var(--mdn-line);font-size:.92rem}
.mdn-times b{font-variant-numeric:tabular-nums}
.mdn-times span{color:var(--mdn-muted)}
@container scroll-state(stuck: top){
.mdn-day h3{padding-block:8px;background:var(--mdn-ink);color:#fff;box-shadow:0 10px 20px -12px rgba(20,22,43,.6)}
.mdn-day h3 span{color:#c7cbe8}
}
@supports (container-type:scroll-state){.mdn-ss .mdn-yes{display:inline}.mdn-ss .mdn-no{display:none}}
/* 2. scroll shadows that appear only while there is more to scroll */
.mdn-shadowbox{container-type:scroll-state}
.mdn-shade{position:sticky;z-index:1;display:block;height:0;pointer-events:none}
.mdn-shade::after{content:"";position:absolute;left:0;right:0;height:22px;opacity:0;transition:opacity .2s}
.mdn-shade.top{top:0}
.mdn-shade.top::after{top:0;background:linear-gradient(rgba(20,22,43,.22),transparent)}
.mdn-shade.bottom{bottom:0}
.mdn-shade.bottom::after{bottom:0;background:linear-gradient(transparent,rgba(20,22,43,.22))}
@container scroll-state(scrollable: top){.mdn-shade.top::after{opacity:1}}
@container scroll-state(scrollable: bottom){.mdn-shade.bottom::after{opacity:1}}
.mdn-notes{--bs-list-group-border-color:var(--mdn-line);--bs-list-group-item-padding-x:20px;--bs-list-group-item-padding-y:12px;--bs-list-group-color:var(--mdn-muted)}
.mdn-notes li{font-size:.92rem}
.mdn-notes b{display:block;color:var(--mdn-ink)}
/* 3. snapped card */
.mdn-snap{padding:8px 4px 16px;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:4px;border-radius:18px}
.mdn-snap ul{list-style:none;padding:0;display:flex;gap:14px;width:max-content}
.mdn-snap li{flex:none;width:clamp(200px,32vw,260px);scroll-snap-align:start;container-type:scroll-state}
.mdn-swatch{--bs-card-border-width:2px;--bs-card-border-color:var(--mdn-line);--bs-card-border-radius:18px;padding:14px;transition:border-color .2s,transform .2s,box-shadow .2s}
.mdn-swatch i{display:flex;height:96px;border-radius:12px;overflow:hidden}
.mdn-swatch i b{flex:1}
.mdn-swatch h3{margin-top:12px;font-size:1rem;font-weight:700}
.mdn-swatch p{font-size:.85rem;color:var(--mdn-muted)}
.mdn-swatch small{display:none;margin-top:6px;font-size:.75rem;font-weight:700;color:var(--mdn-accent)}
@container scroll-state(snapped: x){
.mdn-swatch{border-color:var(--mdn-accent);box-shadow:0 14px 30px -18px rgba(79,70,229,.6)}
.mdn-swatch small{display:block}
}
/* 4. view-timeline ranges */
.mdn-ranges{list-style:none;padding:0;display:grid;gap:12px}
.mdn-ranges .card{--bs-card-border-color:var(--mdn-line);--bs-card-border-radius:16px;display:grid;grid-template-columns:minmax(120px,170px) 1fr;align-items:center;gap:16px;padding:16px 20px}
.mdn-ranges code{font:600 .9rem ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--mdn-pink)}
.mdn-ranges p{font-size:.86rem;color:var(--mdn-muted)}
.mdn-bar{grid-column:1/-1;height:8px;border-radius:4px;background:var(--mdn-accent-soft);overflow:hidden}
.mdn-bar::before{content:"";display:block;height:100%;background:linear-gradient(90deg,var(--mdn-accent),var(--mdn-pink));transform-origin:0 50%}
@keyframes mdn-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@supports (animation-range:entry){
@media (prefers-reduced-motion:no-preference){
.mdn-ranges .card{view-timeline:--mdn-row block}
.mdn-bar::before{animation:mdn-fill linear both;animation-timeline:--mdn-row}
.mdn-bar.entry::before{animation-range:entry 0% entry 100%}
.mdn-bar.contain::before{animation-range:contain 0% contain 100%}
.mdn-bar.cover::before{animation-range:cover 0% cover 100%}
.mdn-bar.exit::before{animation-range:exit 0% exit 100%}
}
.mdn-vt .mdn-yes{display:inline}.mdn-vt .mdn-no{display:none}
}
/* 5. timeline-scope scroll spy */
.mdn-spy{display:grid;grid-template-columns:180px 1fr;gap:16px;timeline-scope:--mdn-s1,--mdn-s2,--mdn-s3,--mdn-s4}
.mdn-spy nav ol{display:grid;gap:6px;position:sticky;top:12px}
.mdn-spy .nav-link{--bs-nav-link-color:var(--mdn-ink);--bs-nav-link-hover-color:var(--mdn-ink);--bs-nav-link-padding-x:14px;--bs-nav-link-padding-y:9px;--bs-nav-pills-border-radius:10px;font-size:.92rem;font-weight:600;border:1px solid var(--mdn-line);background:var(--mdn-card)}
.mdn-spy .nav-link:hover{border-color:var(--mdn-accent)}
.mdn-spy .nav-link:focus-visible{outline:3px solid var(--mdn-accent);outline-offset:2px;box-shadow:none}
.mdn-spy .mdn-box section{min-height:100%;padding:22px 24px;border-bottom:1px solid var(--mdn-line)}
.mdn-spy h3{font-size:1.1rem;font-weight:700}
.mdn-spy section p{margin-top:8px;max-width:56ch;color:var(--mdn-muted)}
.mdn-spy section:nth-child(1){view-timeline:--mdn-s1 block}
.mdn-spy section:nth-child(2){view-timeline:--mdn-s2 block}
.mdn-spy section:nth-child(3){view-timeline:--mdn-s3 block}
.mdn-spy section:nth-child(4){view-timeline:--mdn-s4 block}
@keyframes mdn-on{from,to{background:var(--mdn-ink);color:#fff;border-color:var(--mdn-ink)}}
@supports (timeline-scope:--a){
/* the link is highlighted while its section crosses the middle of the box (sections are one box tall) */
.mdn-spy nav a{animation:mdn-on linear;animation-range:cover 25% cover 75%}
.mdn-spy nav li:nth-child(1) a{animation-timeline:--mdn-s1}
.mdn-spy nav li:nth-child(2) a{animation-timeline:--mdn-s2}
.mdn-spy nav li:nth-child(3) a{animation-timeline:--mdn-s3}
.mdn-spy nav li:nth-child(4) a{animation-timeline:--mdn-s4}
.mdn-ts .mdn-yes{display:inline}.mdn-ts .mdn-no{display:none}
}
@media (max-width:640px){
.mdn-spy{grid-template-columns:1fr}
.mdn-spy nav ol{grid-template-columns:repeat(2,1fr);position:static}
.mdn-ranges .card{grid-template-columns:1fr;gap:6px}
}
@media (prefers-reduced-motion:reduce){
.mdn *,.mdn *::before,.mdn *::after{transition:none!important}
}
Tailwind 4 code
Requires: Tailwind CSS 4
HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#545873] uppercase">scroll-state(stuck): headers that change once they stick</span>
<div class="text-[#14162b]">
<p class="mb-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 text-[.84rem] text-[#545873]">Chrome and Edge 133+. Elsewhere the headers simply stick. <span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold hidden bg-[#dcfce7] text-[#14532d] supports-[container-type:scroll-state]:inline">Works in this browser</span><span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold bg-[#fef3c7] text-[#713f12] supports-[container-type:scroll-state]:hidden">Fallback in this browser</span></p>
<div class="h-[320px] overflow-y-auto overscroll-contain rounded-[18px] border border-[#dfe1ec] bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5]" tabindex="0" role="region" aria-label="Island ferry timetable, scrollable">
<div class="sticky top-0 z-[1] [container-type:scroll-state]"><h3 class="group flex items-center gap-2 border-b border-[#dfe1ec] bg-white px-5 py-3 text-[.95rem] font-bold transition-[padding,background-color,box-shadow,color] duration-200 stuck:bg-[#14162b] stuck:py-2 stuck:text-white stuck:shadow-[0_10px_20px_-12px_rgba(20,22,43,.6)] motion-reduce:transition-none">Monday<span class="ml-auto text-[.78rem] font-semibold text-[#545873] stuck:text-[#c7cbe8]">4 sailings</span></h3></div>
<ul class="px-5 pt-1.5 pb-3.5" aria-label="Monday sailings"><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">06:40</b><span class="text-[#545873]">Oban to Craignure</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">09:15</b><span class="text-[#545873]">Craignure to Oban</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">13:30</b><span class="text-[#545873]">Oban to Craignure</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">17:05</b><span class="text-[#545873]">Craignure to Oban</span></li></ul>
<div class="sticky top-0 z-[1] [container-type:scroll-state]"><h3 class="group flex items-center gap-2 border-b border-[#dfe1ec] bg-white px-5 py-3 text-[.95rem] font-bold transition-[padding,background-color,box-shadow,color] duration-200 stuck:bg-[#14162b] stuck:py-2 stuck:text-white stuck:shadow-[0_10px_20px_-12px_rgba(20,22,43,.6)] motion-reduce:transition-none">Tuesday<span class="ml-auto text-[.78rem] font-semibold text-[#545873] stuck:text-[#c7cbe8]">3 sailings</span></h3></div>
<ul class="px-5 pt-1.5 pb-3.5" aria-label="Tuesday sailings"><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">07:10</b><span class="text-[#545873]">Oban to Craignure</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">12:00</b><span class="text-[#545873]">Craignure to Oban</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">18:20</b><span class="text-[#545873]">Oban to Craignure</span></li></ul>
<div class="sticky top-0 z-[1] [container-type:scroll-state]"><h3 class="group flex items-center gap-2 border-b border-[#dfe1ec] bg-white px-5 py-3 text-[.95rem] font-bold transition-[padding,background-color,box-shadow,color] duration-200 stuck:bg-[#14162b] stuck:py-2 stuck:text-white stuck:shadow-[0_10px_20px_-12px_rgba(20,22,43,.6)] motion-reduce:transition-none">Wednesday<span class="ml-auto text-[.78rem] font-semibold text-[#545873] stuck:text-[#c7cbe8]">4 sailings</span></h3></div>
<ul class="px-5 pt-1.5 pb-3.5" aria-label="Wednesday sailings"><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">06:40</b><span class="text-[#545873]">Oban to Craignure</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">10:45</b><span class="text-[#545873]">Craignure to Oban</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">14:30</b><span class="text-[#545873]">Oban to Craignure</span></li><li class="flex justify-between gap-3 border-b border-dashed border-[#dfe1ec] py-[9px] text-[.92rem]"><b class="tabular-nums">19:00</b><span class="text-[#545873]">Craignure to Oban</span></li></ul>
</div>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#545873] uppercase">scroll-state(scrollable): shadows only while there is more</span>
<div class="text-[#14162b]">
<p class="mb-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 text-[.84rem] text-[#545873]">Chrome and Edge 133+. Elsewhere the list scrolls without the shadows. <span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold hidden bg-[#dcfce7] text-[#14532d] supports-[container-type:scroll-state]:inline">Works in this browser</span><span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold bg-[#fef3c7] text-[#713f12] supports-[container-type:scroll-state]:hidden">Fallback in this browser</span></p>
<div class="h-[320px] overflow-y-auto overscroll-contain rounded-[18px] border border-[#dfe1ec] bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5] [container-type:scroll-state]" tabindex="0" role="region" aria-label="Release notes, scrollable">
<i class="pointer-events-none sticky z-[1] block h-0 after:absolute after:inset-x-0 after:h-[22px] after:opacity-0 after:transition-opacity after:duration-200 after:content-[''] motion-reduce:after:transition-none top-0 after:top-0 after:bg-linear-to-b after:from-[rgba(20,22,43,.22)] after:to-transparent scrollable-top:after:opacity-100" aria-hidden="true"></i>
<ul class="px-5 py-2">
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">4.2.0 · 14 March</b>Offline mode for the timetable, with the next seven days saved on the phone.</li>
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">4.1.3 · 2 March</b>Fixed a crash when a sailing was cancelled while the page was open.</li>
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">4.1.2 · 20 February</b>Times now follow the phone's 12 or 24 hour setting.</li>
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">4.1.0 · 6 February</b>Live vessel positions on the map, updated every 30 seconds.</li>
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">4.0.1 · 22 January</b>Bigger tap targets on the booking screen.</li>
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">4.0.0 · 9 January</b>A new design with dark mode and larger text options.</li>
<li class="border-b border-[#dfe1ec] py-3 text-[.92rem] text-[#545873]"><b class="block text-[#14162b]">3.9.4 · 12 December</b>Winter timetable added for all eleven routes.</li>
</ul>
<i class="pointer-events-none sticky z-[1] block h-0 after:absolute after:inset-x-0 after:h-[22px] after:opacity-0 after:transition-opacity after:duration-200 after:content-[''] motion-reduce:after:transition-none bottom-0 after:bottom-0 after:bg-linear-to-t after:from-[rgba(20,22,43,.22)] after:to-transparent scrollable-bottom:after:opacity-100" aria-hidden="true"></i>
</div>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#545873] uppercase">scroll-state(snapped): the card that is snapped</span>
<div class="text-[#14162b]">
<p class="mb-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 text-[.84rem] text-[#545873]">Chrome and Edge 133+. Elsewhere it is a normal snap carousel. <span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold hidden bg-[#dcfce7] text-[#14532d] supports-[container-type:scroll-state]:inline">Works in this browser</span><span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold bg-[#fef3c7] text-[#713f12] supports-[container-type:scroll-state]:hidden">Fallback in this browser</span></p>
<div class="snap-x snap-mandatory scroll-px-1 overflow-x-auto rounded-[18px] px-1 pt-2 pb-4 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5]" tabindex="0" role="region" aria-label="Theme presets, scrolls sideways"><ul class="flex w-max gap-3.5">
<li class="w-[clamp(200px,32vw,260px)] flex-none snap-start [container-type:scroll-state]"><div class="rounded-[18px] border-2 border-[#dfe1ec] bg-white p-3.5 transition-[border-color,box-shadow] duration-200 snapped:border-[#4f46e5] snapped:shadow-[0_14px_30px_-18px_rgba(79,70,229,.6)] motion-reduce:transition-none"><i class="flex h-24 overflow-hidden rounded-xl" aria-hidden="true"><b class="flex-1" style="background:#1e1b4b"></b><b class="flex-1" style="background:#4f46e5"></b><b class="flex-1" style="background:#a5b4fc"></b><b class="flex-1" style="background:#eef2ff"></b></i><h3 class="mt-3 font-bold">Dusk</h3><p class="text-[.85rem] text-[#545873]">Indigo with a pale lilac ground</p><small class="mt-1.5 hidden text-[.75rem] font-bold text-[#4f46e5] snapped:block">Snapped</small></div></li>
<li class="w-[clamp(200px,32vw,260px)] flex-none snap-start [container-type:scroll-state]"><div class="rounded-[18px] border-2 border-[#dfe1ec] bg-white p-3.5 transition-[border-color,box-shadow] duration-200 snapped:border-[#4f46e5] snapped:shadow-[0_14px_30px_-18px_rgba(79,70,229,.6)] motion-reduce:transition-none"><i class="flex h-24 overflow-hidden rounded-xl" aria-hidden="true"><b class="flex-1" style="background:#14532d"></b><b class="flex-1" style="background:#16a34a"></b><b class="flex-1" style="background:#bbf7d0"></b><b class="flex-1" style="background:#f0fdf4"></b></i><h3 class="mt-3 font-bold">Moss</h3><p class="text-[.85rem] text-[#545873]">Deep greens for calm dashboards</p><small class="mt-1.5 hidden text-[.75rem] font-bold text-[#4f46e5] snapped:block">Snapped</small></div></li>
<li class="w-[clamp(200px,32vw,260px)] flex-none snap-start [container-type:scroll-state]"><div class="rounded-[18px] border-2 border-[#dfe1ec] bg-white p-3.5 transition-[border-color,box-shadow] duration-200 snapped:border-[#4f46e5] snapped:shadow-[0_14px_30px_-18px_rgba(79,70,229,.6)] motion-reduce:transition-none"><i class="flex h-24 overflow-hidden rounded-xl" aria-hidden="true"><b class="flex-1" style="background:#7c2d12"></b><b class="flex-1" style="background:#ea580c"></b><b class="flex-1" style="background:#fed7aa"></b><b class="flex-1" style="background:#fff7ed"></b></i><h3 class="mt-3 font-bold">Ember</h3><p class="text-[.85rem] text-[#545873]">Warm orange with a cream ground</p><small class="mt-1.5 hidden text-[.75rem] font-bold text-[#4f46e5] snapped:block">Snapped</small></div></li>
<li class="w-[clamp(200px,32vw,260px)] flex-none snap-start [container-type:scroll-state]"><div class="rounded-[18px] border-2 border-[#dfe1ec] bg-white p-3.5 transition-[border-color,box-shadow] duration-200 snapped:border-[#4f46e5] snapped:shadow-[0_14px_30px_-18px_rgba(79,70,229,.6)] motion-reduce:transition-none"><i class="flex h-24 overflow-hidden rounded-xl" aria-hidden="true"><b class="flex-1" style="background:#831843"></b><b class="flex-1" style="background:#db2777"></b><b class="flex-1" style="background:#fbcfe8"></b><b class="flex-1" style="background:#fdf2f8"></b></i><h3 class="mt-3 font-bold">Coral</h3><p class="text-[.85rem] text-[#545873]">Pinks that work on light pages</p><small class="mt-1.5 hidden text-[.75rem] font-bold text-[#4f46e5] snapped:block">Snapped</small></div></li>
<li class="w-[clamp(200px,32vw,260px)] flex-none snap-start [container-type:scroll-state]"><div class="rounded-[18px] border-2 border-[#dfe1ec] bg-white p-3.5 transition-[border-color,box-shadow] duration-200 snapped:border-[#4f46e5] snapped:shadow-[0_14px_30px_-18px_rgba(79,70,229,.6)] motion-reduce:transition-none"><i class="flex h-24 overflow-hidden rounded-xl" aria-hidden="true"><b class="flex-1" style="background:#0c4a6e"></b><b class="flex-1" style="background:#0284c7"></b><b class="flex-1" style="background:#bae6fd"></b><b class="flex-1" style="background:#f0f9ff"></b></i><h3 class="mt-3 font-bold">Harbour</h3><p class="text-[.85rem] text-[#545873]">Sea blues with a white ground</p><small class="mt-1.5 hidden text-[.75rem] font-bold text-[#4f46e5] snapped:block">Snapped</small></div></li>
</ul></div>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#545873] uppercase">view-timeline ranges: one animation, four ranges</span>
<div class="text-[#14162b]">
<p class="mb-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 text-[.84rem] text-[#545873]">Chrome 115+, Safari 26+. Elsewhere the bars are drawn full. <span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold hidden bg-[#dcfce7] text-[#14532d] supports-[animation-range:entry]:inline">Works in this browser</span><span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold bg-[#fef3c7] text-[#713f12] supports-[animation-range:entry]:hidden">Fallback in this browser</span></p>
<ul class="grid gap-3">
<li class="grid grid-cols-[minmax(120px,170px)_1fr] items-center gap-4 rounded-2xl border border-[#dfe1ec] bg-white px-5 py-4 max-sm:grid-cols-1 max-sm:gap-1.5" data-mdn-row><code class="font-mono text-[.9rem] font-semibold text-[#be185d]">entry</code><p class="text-[.86rem] text-[#545873]">Fills while the row slides in from the bottom edge.</p><i class="col-span-full h-2 overflow-hidden rounded bg-[#eef0ff]" aria-hidden="true"><b class="block h-full origin-left bg-linear-to-r from-[#4f46e5] to-[#be185d]" data-mdn-range="entry"></b></i></li>
<li class="grid grid-cols-[minmax(120px,170px)_1fr] items-center gap-4 rounded-2xl border border-[#dfe1ec] bg-white px-5 py-4 max-sm:grid-cols-1 max-sm:gap-1.5" data-mdn-row><code class="font-mono text-[.9rem] font-semibold text-[#be185d]">contain</code><p class="text-[.86rem] text-[#545873]">Fills while the whole row is on screen.</p><i class="col-span-full h-2 overflow-hidden rounded bg-[#eef0ff]" aria-hidden="true"><b class="block h-full origin-left bg-linear-to-r from-[#4f46e5] to-[#be185d]" data-mdn-range="contain"></b></i></li>
<li class="grid grid-cols-[minmax(120px,170px)_1fr] items-center gap-4 rounded-2xl border border-[#dfe1ec] bg-white px-5 py-4 max-sm:grid-cols-1 max-sm:gap-1.5" data-mdn-row><code class="font-mono text-[.9rem] font-semibold text-[#be185d]">cover</code><p class="text-[.86rem] text-[#545873]">Fills from first pixel in to last pixel out.</p><i class="col-span-full h-2 overflow-hidden rounded bg-[#eef0ff]" aria-hidden="true"><b class="block h-full origin-left bg-linear-to-r from-[#4f46e5] to-[#be185d]" data-mdn-range="cover"></b></i></li>
<li class="grid grid-cols-[minmax(120px,170px)_1fr] items-center gap-4 rounded-2xl border border-[#dfe1ec] bg-white px-5 py-4 max-sm:grid-cols-1 max-sm:gap-1.5" data-mdn-row><code class="font-mono text-[.9rem] font-semibold text-[#be185d]">exit</code><p class="text-[.86rem] text-[#545873]">Fills while the row leaves at the top edge.</p><i class="col-span-full h-2 overflow-hidden rounded bg-[#eef0ff]" aria-hidden="true"><b class="block h-full origin-left bg-linear-to-r from-[#4f46e5] to-[#be185d]" data-mdn-range="exit"></b></i></li>
</ul>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#545873] uppercase">timeline-scope: a scroll spy with no JavaScript</span>
<div class="text-[#14162b]">
<p class="mb-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 text-[.84rem] text-[#545873]">Chrome 116+, Safari 26+. Elsewhere the links still jump to each step, without the highlight. <span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold hidden bg-[#dcfce7] text-[#14532d] supports-[timeline-scope:--a]:inline">Works in this browser</span><span class="rounded-full px-2.5 py-0.5 text-[.76rem] font-bold bg-[#fef3c7] text-[#713f12] supports-[timeline-scope:--a]:hidden">Fallback in this browser</span></p>
<div class="grid grid-cols-[180px_1fr] gap-4 [timeline-scope:--mdn-s1,--mdn-s2,--mdn-s3,--mdn-s4] max-sm:grid-cols-1" data-mdn-spy>
<nav aria-label="Sourdough steps"><ol class="sticky top-3 grid gap-1.5 max-sm:static max-sm:grid-cols-2"><li><a class="block rounded-[10px] border border-[#dfe1ec] bg-white px-3.5 py-[9px] text-[.92rem] font-semibold hover:border-[#4f46e5] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" href="#mdn-starter">1. Starter</a></li><li><a class="block rounded-[10px] border border-[#dfe1ec] bg-white px-3.5 py-[9px] text-[.92rem] font-semibold hover:border-[#4f46e5] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" href="#mdn-mix">2. Mix</a></li><li><a class="block rounded-[10px] border border-[#dfe1ec] bg-white px-3.5 py-[9px] text-[.92rem] font-semibold hover:border-[#4f46e5] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" href="#mdn-proof">3. Proof</a></li><li><a class="block rounded-[10px] border border-[#dfe1ec] bg-white px-3.5 py-[9px] text-[.92rem] font-semibold hover:border-[#4f46e5] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]" href="#mdn-bake">4. Bake</a></li></ol></nav>
<div class="h-[320px] overflow-y-auto overscroll-contain rounded-[18px] border border-[#dfe1ec] bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#4f46e5]" tabindex="0" role="region" aria-label="Sourdough method, scrollable">
<section class="min-h-full border-b border-[#dfe1ec] px-6 py-[22px] [view-timeline:--mdn-s1_block]" id="mdn-starter" aria-labelledby="mdn-starter-h"><h3 class="text-[1.1rem] font-bold" id="mdn-starter-h">1. Feed the starter</h3><p class="mt-2 max-w-[56ch] text-[#545873]">The night before, mix 50 g of starter with 50 g of flour and 50 g of water. By morning it should have doubled and smell sweet and sour.</p><p class="mt-2 max-w-[56ch] text-[#545873]">Drop a spoonful in water: if it floats, it is ready.</p></section>
<section class="min-h-full border-b border-[#dfe1ec] px-6 py-[22px] [view-timeline:--mdn-s2_block]" id="mdn-mix" aria-labelledby="mdn-mix-h"><h3 class="text-[1.1rem] font-bold" id="mdn-mix-h">2. Mix and fold</h3><p class="mt-2 max-w-[56ch] text-[#545873]">Stir 100 g of starter into 350 g of water, add 500 g of strong flour and 10 g of salt, and mix until no dry flour is left.</p><p class="mt-2 max-w-[56ch] text-[#545873]">Over the next two hours, stretch and fold the dough four times.</p></section>
<section class="min-h-full border-b border-[#dfe1ec] px-6 py-[22px] [view-timeline:--mdn-s3_block]" id="mdn-proof" aria-labelledby="mdn-proof-h"><h3 class="text-[1.1rem] font-bold" id="mdn-proof-h">3. Shape and proof</h3><p class="mt-2 max-w-[56ch] text-[#545873]">Shape a tight round, put it seam side up in a floured basket, cover it and leave it in the fridge for 12 to 16 hours.</p><p class="mt-2 max-w-[56ch] text-[#545873]">Cold proofing deepens the flavour and makes scoring easier.</p></section>
<section class="min-h-full border-b border-[#dfe1ec] px-6 py-[22px] [view-timeline:--mdn-s4_block]" id="mdn-bake" aria-labelledby="mdn-bake-h"><h3 class="text-[1.1rem] font-bold" id="mdn-bake-h">4. Bake</h3><p class="mt-2 max-w-[56ch] text-[#545873]">Heat a lidded pot at 250 °C for 45 minutes. Tip the dough in, score it, bake 20 minutes with the lid on and 25 with it off.</p><p class="mt-2 max-w-[56ch] text-[#545873]">Let it cool for an hour before cutting.</p></section>
</div>
</div>
</div></div>
CSS
/* scroll-state() container queries as Tailwind variants (Chrome and Edge 133+; other browsers ignore them):
put [container-type:scroll-state] on the sticky, scrolling or snapping element, then use stuck:, snapped:,
scrollable-top: and scrollable-bottom: on its children. Support badges use the built-in supports-[...] variant. */
@custom-variant stuck { @container scroll-state(stuck: top) { @slot; } }
@custom-variant snapped { @container scroll-state(snapped: x) { @slot; } }
@custom-variant scrollable-top { @container scroll-state(scrollable: top) { @slot; } }
@custom-variant scrollable-bottom { @container scroll-state(scrollable: bottom) { @slot; } }
/* view-timeline ranges (Chrome 115+, Safari 26+) and the timeline-scope scroll spy (Chrome 116+, Safari 26+) */
@keyframes mdn-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@keyframes mdn-on{from,to{background:#14162b;color:#fff;border-color:#14162b}}
@supports (animation-range:entry){
@media (prefers-reduced-motion:no-preference){
[data-mdn-row]{view-timeline:--mdn-row block}
[data-mdn-range]{animation:mdn-fill linear both;animation-timeline:--mdn-row}
[data-mdn-range="entry"]{animation-range:entry 0% entry 100%}
[data-mdn-range="contain"]{animation-range:contain 0% contain 100%}
[data-mdn-range="cover"]{animation-range:cover 0% cover 100%}
[data-mdn-range="exit"]{animation-range:exit 0% exit 100%}
}
}
@supports (timeline-scope:--a){
[data-mdn-spy] nav a{animation:mdn-on linear;animation-range:cover 25% cover 75%}
[data-mdn-spy] nav li:nth-child(1) a{animation-timeline:--mdn-s1}
[data-mdn-spy] nav li:nth-child(2) a{animation-timeline:--mdn-s2}
[data-mdn-spy] nav li:nth-child(3) a{animation-timeline:--mdn-s3}
[data-mdn-spy] nav li:nth-child(4) a{animation-timeline:--mdn-s4}
}
About this set
Five scroll effects that usually need JavaScript, built with newer CSS, each with a line that names its baseline and a badge that uses @supports to say whether your browser runs it. Stuck headers is a ferry timetable whose day headings turn dark and compact once they stick, using a scroll-state(stuck: top) container query. Scroll shadows is a release notes box that shows a shadow at the top or bottom only while there is more to scroll that way, using scroll-state(scrollable). Snapped card is a carousel of colour presets that outlines and labels the card that is snapped, using scroll-state(snapped: x). View-timeline ranges runs one fill animation over entry, contain, cover and exit so you can see how each range behaves. Scroll spy is a sourdough method in a scroll container whose step links sit outside it and still highlight the step in view, because timeline-scope lifts each section’s view timeline up to the shared parent. Every effect has a plain fallback: headers that just stick, a list without shadows, a normal snap carousel, full bars and links that still jump to each step.
What’s included
scroll-state() queries for stuck headers, scroll shadows and snapped cards
view-timeline ranges entry, contain, cover and exit side by side
Every scroll container is a labelled region that takes keyboard focus with a visible outline. The scroll spy links are real in-page links that work without the highlight, and the highlight is a visual aid only. Support notes are visible text, not just colour. Under prefers-reduced-motion the range bars stand still and transitions are off, while the stuck, snapped and current-step styles change instantly.
Customise it
Change --mdn-accent and --mdn-ink for the highlight colours, the box height on .mdn-box, and the animation-range on the scroll spy links if your sections are not one box tall. In Tailwind, use the custom variants from the style block on children of any element with [container-type:scroll-state].