Sticky-Header, der schrumpft bzw. sich beim Andocken umfärbt
Der Header schrumpft scrollgekoppelt von 168 auf 64 px, die Marke von 4.5625 auf 3.125 rem, und bekommt beim Andocken eine Glasfläche mit Haarlinie.
Am Seitenanfang ist der Header großzügig: viel Raum, große Marke, manchmal die ganze Wortmarke. Beim Scrollen schrumpft er an den Scroll gekoppelt auf eine kompakte Höhe. Das Logo skaliert mit oder wechselt zur Bildmarke. Sobald er andockt, bekommt er Hintergrund, Trennlinie oder Blur. Sonderfall Studio-Seiten: die große Wortmarke aus dem Hero wandert beim Scrollen in die Logo-Position.
Beim Einstieg steht die Marke groß da, beim Lesen nimmt der Header kaum Platz weg. Weil der Übergang am Scroll hängt, wirkt er flüssig statt umgeschaltet. Glas und Haarlinie trennen den Header erst dann vom Inhalt, wenn Inhalt darunter liegt.
Wann einsetzen
Marken mit starker Wortmarke oder Bildmarke, Editorial-Seiten, Portfolios und Shops mit großem Logo. Auch bei leisen Seiten, wenn der Header am Anfang die Visitenkarte ist.
Wann nicht
Wenn der Header ohnehin klein ist. Wenn die Umsetzung über height läuft und dabei der Inhalt ruckelt; dann Höhe konstant halten und transform nutzen.
Varianten
- Schrumpfen: Höhe und Marke scrollgekoppelt verkleinern (Chrome-Anleitung 200 → 50 px über 150 px; Stewart & Partners Logo 4.5625 → 3.125 rem über 500 px).
- Umfärben beim Andocken: nur Hintergrund, Linie oder Inhalt wechseln, sobald scroll-state(stuck: top) greift.
- Wortmarke wandert ins Logo: der große Name aus dem Hero skaliert und verschiebt sich scrollgekoppelt in den Header.
- Glas statt Fläche: halbtransparenter Hintergrund mit backdrop-filter: blur(12px), eingeblendet in 200 ms (Resend, Clay); auch als Progressive Blur.
Umsetzung
Reines CSS über eine Scroll-Timeline: @property --p { syntax: '<number>'; inherits: true; initial-value: 0 } @keyframes p { to { --p: 1 } } header { position: fixed; animation: p linear both; animation-timeline: scroll(root block); animation-range: 0px 500px }. Alle Teile rechnen aus --p nur transform und opacity, die Höhe bleibt konstant. Das ist schneller als height zu animieren. Die Chrome-Anleitung animiert height direkt (200 → 50 px über 150 px). Andocken: header { position: sticky; top: 0; container-type: scroll-state } und @container scroll-state(stuck: top) { .innen { background: …; border-bottom: … } }. Der Stil muss auf Nachfahren liegen. Support prüfen mit CSS.supports('animation-timeline: scroll()') und CSS.supports('container-type: scroll-state'), sonst --p bzw. eine Klasse per JS setzen. Fallstrick: den animation-Shorthand vor animation-timeline schreiben, sonst setzt er sie zurück. Mit GSAP: ScrollTrigger mit start 'top top', end '+=500', scrub und ease 'none'.
Robust umsetzen
Telefon. Geringere Ausgangshöhe (z. B. 120 → 56 px) und kleinere Marke (Stewart & Partners mobil 4 → 3 rem). Nebenzeilen wie Status früh ausblenden. Die wandernde Wortmarke kleiner beginnen lassen oder weglassen.
Ohne Bewegung. Statt kontinuierlichem Schrumpfen zwei feste Zustände ohne Übergang, z. B. Wechsel nach halbem Scrollweg.
Barrierefrei. Der Logo-Link behält seinen Namen (aria-label), auch wenn nur die Bildmarke sichtbar ist. Text nicht unter lesbare Größe skalieren (Endgröße mindestens 16 px). Kontrast in beiden Zuständen prüfen, besonders auf Glas über Bildern. Die Navigation bleibt in beiden Zuständen per Tastatur erreichbar.
Code
<!DOCTYPE html>
<!--
Sticky-Header, der schrumpft bzw. sich beim Andocken umfärbt
Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/sticky-shrink-header/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-variante="schrumpfen">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sticky-Header, der schrumpft bzw. sich beim Andocken umfärbt · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Host Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/host-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Stille Visitenkarte: Graustufen, eine Farbe für Fokus und Link-Hover */
:root {
--grund: #fcfcfc; --text: #0b0b09; --leise: #646464; --linie: #e8e8e8; --flaeche: #f0f0f0; --akzent: #2f5bea;
--glas: rgba(252, 252, 252, .74);
--ui: cubic-bezier(0.165, 0.84, 0.44, 1); /* easeOutQuart, Hover/Farbe */
--glas-kurve: ease-in-out; /* gemessen: Resend, Glas-Layer 200 ms */
--rand: clamp(16px, 4vw, 40px);
--seite: 1072px; --spalte: 640px;
/* Variante A · Schrumpfen: Höhe 168 → 64 px, Marke 4.5625rem → 3.125rem (gemessen: Stewart & Partners) */
--h0: 168px; --h1: 64px; --k: 0.381; /* k = h1 / h0 */
--mw: 4.5625rem; --m: 0.3151; --mx: 1.4375rem; /* Markenbreite, Schrumpfanteil, Versatz */
/* Variante C · Wortmarke: 64 px → 16 px über 150 px Scrollweg (Chrome-Anleitung) */
--n0: 64px; --ns: 0.75; --y0: 112px; --y1: 24px;
}
@media (prefers-color-scheme: dark) {
:root { --grund: #0b0b09; --text: #ededeb; --leise: #989898; --linie: #252525; --flaeche: #181816; --akzent: #7b97ff; --glas: rgba(11, 11, 9, .72); }
}
:root[data-variante='wortmarke'] { --h0: 216px; --k: 0.2963; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font: 400 16px/1.6 'Host Grotesk', system-ui, sans-serif; -webkit-font-smoothing: antialiased; font-feature-settings: 'tnum' 0; }
body { min-height: 100svh; overflow-x: clip; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; border-radius: 4px; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
em { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-weight: 400; font-size: 1.05em; font-variation-settings: 'opsz' 24; letter-spacing: 0; }
.innen { width: min(100%, var(--seite)); margin-inline: auto; padding-inline: var(--rand); }
/* ── Fortschritt p (0 → 1) als registrierte Zahl, damit alle Teile daraus rechnen können ── */
@property --p { syntax: '<number>'; inherits: true; initial-value: 0; }
@keyframes andocken { from { --p: 0; } to { --p: 1; } }
@supports (animation-timeline: scroll()) {
/* Shorthand zuerst, animation-timeline danach (sonst setzt der Shorthand sie zurück) */
.kopf { animation: andocken linear both; animation-timeline: scroll(root block); animation-range: 0px 500px; }
[data-variante='wortmarke'] .kopf { animation-range: 0px 150px; }
}
/* ── Header A + C: fest, Höhe konstant, nur transform ── */
.kopf { position: fixed; inset: 0 0 auto; height: var(--h0); z-index: 20; pointer-events: none; }
.kopf a, .kopf button { pointer-events: auto; }
.kopf__glas { position: absolute; inset: 0; transform-origin: 50% 0; transform: scaleY(calc(1 - var(--p) * (1 - var(--k))));
background: var(--glas); -webkit-backdrop-filter: blur(12px) saturate(1.4); backdrop-filter: blur(12px) saturate(1.4);
opacity: 0; transition: opacity 200ms var(--glas-kurve); }
.kopf__linie { position: absolute; left: 0; right: 0; top: calc(var(--h0) - 1px); height: 1px; background: var(--linie);
transform: translateY(calc(var(--p) * (var(--h1) - var(--h0)))); opacity: 0; transition: opacity 200ms var(--glas-kurve); }
.angedockt .kopf__glas, .angedockt .kopf__linie { opacity: 1; }
.kopf__zeile { position: absolute; inset: 0 0 auto; height: var(--h0); display: flex; align-items: center; justify-content: space-between; gap: 24px;
transform: translateY(calc(var(--p) * (var(--h1) - var(--h0)) / 2)); }
.marke { display: flex; align-items: center; gap: 14px; }
.marke__zeichen { width: var(--mw); height: auto; flex: none; transform-origin: 0 50%; transform: scale(calc(1 - var(--p) * var(--m))); }
.marke__zeichen { fill: none; stroke: currentColor; stroke-width: 3.4; stroke-linecap: round; }
.marke__text { display: grid; line-height: 1.3; transform: translate(calc(var(--p) * var(--mx) * -1), calc(var(--p) * 9px)); }
.marke__name { font-weight: 500; }
.marke__rolle { color: var(--leise); font-size: .875rem; opacity: calc(1 - var(--p) * 2.5); }
.nav { display: flex; align-items: center; gap: 28px; font-size: .875rem; }
.nav a { color: var(--leise); transition: color 120ms var(--ui); }
.nav a:hover { color: var(--text); }
.status { display: flex; align-items: center; gap: 8px; opacity: calc(1 - var(--p) * 2.5); }
.status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--text); }
/* Variante C: die Wortmarke selbst wandert in den Header */
.wortmarke { position: absolute; left: calc(max(0px, (100% - var(--seite)) / 2) + var(--rand)); top: 0; font-size: var(--n0); font-weight: 500; line-height: 1; letter-spacing: -.035em; white-space: nowrap;
transform-origin: 0 0; transform: translateY(calc(var(--y0) + var(--p) * (var(--y1) - var(--y0)))) scale(calc(1 - var(--p) * var(--ns))); display: none; }
[data-variante='wortmarke'] .wortmarke { display: block; }
[data-variante='wortmarke'] .marke { visibility: hidden; }
[data-variante='wortmarke'] .kopf__zeile { height: var(--h1); transform: none; }
[data-variante='wortmarke'] .status { display: none; }
[data-variante='andocken'] .kopf { display: none; }
/* ── Seite ── */
main { padding-top: calc(var(--h0) + 40px); padding-bottom: 120px; }
[data-variante='wortmarke'] main { padding-top: calc(var(--h0) + 8px); }
[data-variante='andocken'] main { padding-top: 104px; }
.spalte { max-width: var(--spalte); }
.intro .mono { margin-bottom: 20px; }
.intro h1 { font-size: clamp(1.25rem, 2vw, 1.5rem); font-weight: 500; line-height: 1.4; letter-spacing: -.01em; text-wrap: pretty; }
.satz-c { display: none; }
[data-variante='wortmarke'] .satz-c { display: inline; }
[data-variante='wortmarke'] .satz-a { display: none; }
.abschnitt { margin-top: 112px; }
.abschnitt h2 { font-size: 1rem; font-weight: 500; color: var(--leise); margin-bottom: 20px; }
.abschnitt p + p { margin-top: 1em; }
.text { text-wrap: pretty; }
.liste li + li { border-top: 1px solid var(--linie); }
.liste a { display: grid; grid-template-columns: 1fr auto; gap: 2px 24px; padding: 14px 12px; margin-inline: -12px; border-radius: 8px; transition: background-color 120ms var(--ui); }
.liste a:hover { background: var(--flaeche); }
.liste li:has(a:hover) + li, .liste li:has(+ li a:hover) { border-color: transparent; }
.liste .t { font-weight: 500; }
.liste .d { grid-column: 1; color: var(--leise); font-size: .875rem; }
.liste .j { grid-column: 2; grid-row: 1; color: var(--leise); font-size: .875rem; font-variant-numeric: tabular-nums; text-align: right; }
.zitat { font-size: 1.125rem; line-height: 1.55; text-wrap: pretty; }
.zitat + .mono { margin-top: 16px; }
.kontakt { display: grid; gap: 6px; }
.kopieren { font: inherit; font-size: 1.25rem; font-weight: 500; color: var(--text); background: none; border: 0; cursor: pointer; display: inline-flex; align-items: baseline; gap: 12px; justify-self: start; padding: 4px 0; }
.kopieren:hover .adresse { color: var(--akzent); }
.adresse { transition: color 120ms var(--ui); }
.kopieren .mono { transition: opacity 160ms var(--ui); }
.fuss { margin-top: 112px; padding-top: 20px; border-top: 1px solid var(--linie); display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; font-size: .875rem; color: var(--leise); }
/* ── Variante B: Leiste dockt an (scroll-state-Query, Fallback per Klasse) ── */
.leiste { display: none; position: sticky; top: 0; z-index: 20; margin-top: 64px; container-type: scroll-state; }
[data-variante='andocken'] .leiste { display: block; }
.leiste__innen { position: relative; height: 56px; display: flex; align-items: center; justify-content: space-between; gap: 16px; border-top: 1px solid var(--linie);
transition: border-color 200ms var(--glas-kurve); }
.leiste__innen::before { content: ''; position: absolute; inset: -1px 0 0; z-index: -1; background: var(--glas); -webkit-backdrop-filter: blur(12px) saturate(1.4); backdrop-filter: blur(12px) saturate(1.4);
border-bottom: 1px solid var(--linie); opacity: 0; transition: opacity 200ms var(--glas-kurve); }
.leiste__links { position: relative; height: 100%; flex: none; min-width: 120px; }
.leiste__links > * { position: absolute; left: 0; top: 50%; translate: 0 -50%; white-space: nowrap; transition: opacity 200ms var(--glas-kurve), transform 200ms var(--glas-kurve); }
.leiste__name { display: flex; align-items: center; gap: 10px; font-weight: 500; font-size: .9375rem; opacity: 0; transform: translateY(6px); }
.leiste__name svg { width: 2rem; height: auto; }
.leiste__name svg { fill: none; stroke: currentColor; stroke-width: 4; stroke-linecap: round; }
@container scroll-state(stuck: top) {
.leiste__innen { border-top-color: transparent; }
.leiste__innen::before { opacity: 1; }
.leiste__name { opacity: 1; transform: none; }
.leiste__inhalt { opacity: 0; transform: translateY(-6px); }
}
.leiste.angedockt .leiste__innen { border-top-color: transparent; }
.leiste.angedockt .leiste__innen::before { opacity: 1; }
.leiste.angedockt .leiste__name { opacity: 1; transform: none; }
.leiste.angedockt .leiste__inhalt { opacity: 0; transform: translateY(-6px); }
/* volle Breite des Glases, Inhalt in der Seitenbreite */
.leiste .innen { position: relative; }
.leiste__innen::before { left: calc(50% - 50vw); right: calc(50% - 50vw); }
/* ── Bedienfeld (dezent) ── */
.steuer { position: fixed; right: 16px; bottom: 16px; z-index: 30; display: grid; gap: 10px; width: min(360px, calc(100% - 32px)); padding: 12px; border: 1px solid var(--linie); border-radius: 12px; background: var(--glas); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 28px; padding: 0 10px; cursor: pointer; transition: color 120ms var(--ui), background-color 120ms var(--ui); }
.modi button:hover { color: var(--text); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }
.werte { display: grid; gap: 2px; text-transform: none; letter-spacing: .02em; font-size: 10.5px; }
.werte b { color: var(--text); font-weight: 500; font-variant-numeric: tabular-nums; }
.werte__live { display: flex; gap: 14px; }
/* ── Telefon: geringere Ausgangshöhe, Marke 4rem → 3rem (Stewart & Partners mobil) ── */
@media (max-width: 640px) {
:root { --h0: 120px; --h1: 56px; --k: 0.4667; --mw: 4rem; --m: 0.25; --mx: 1rem; --n0: 40px; --ns: 0.6; --y0: 64px; --y1: 20px; }
:root[data-variante='wortmarke'] { --h0: 128px; --k: 0.4375; }
[data-variante='wortmarke'] main { padding-top: calc(var(--h0) + 24px); }
.nav { gap: 18px; }
.nav .weg { display: none; }
.status, .breit { display: none; }
.marke { gap: 10px; }
.marke__rolle { font-size: .8125rem; }
main { padding-top: calc(var(--h0) + 24px); }
.abschnitt { margin-top: 88px; }
.leiste__links { min-width: 112px; }
.leiste .nav { gap: 16px; }
.steuer { right: 12px; left: 12px; bottom: 12px; width: auto; padding: 10px; gap: 8px; }
.werte__regel { display: none; }
}
@media (hover: none) {
.liste a:hover { background: none; }
.liste a:active { background: var(--flaeche); }
.nav a:hover { color: var(--leise); }
}
/* ── Reduzierte Bewegung: zwei feste Zustände ohne Übergang (p setzt das Skript auf 0 oder 1) ── */
.reduziert .kopf { animation: none !important; }
.reduziert *, .reduziert *::before { transition: none !important; }
/* ── Vorschau: nur die Bühne ── */
.vorschau body { overflow: hidden; }
.vorschau .modi, .vorschau .werte__regel { display: none; }
.vorschau .steuer { width: auto; padding: 8px 12px; }
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="zeichen" viewBox="0 0 73 40"><path d="M3 37V19.5a8.25 8.25 0 0 1 16.5 0V37M19.5 19.5a8.25 8.25 0 0 1 16.5 0V37"/><circle cx="54.5" cy="25" r="12"/></symbol>
</svg>
<header class="kopf" data-kopf>
<div class="kopf__glas"></div>
<div class="kopf__linie"></div>
<div class="kopf__zeile innen">
<a class="marke" href="#oben" aria-label="Marit Oswald, zum Seitenanfang">
<svg class="marke__zeichen" viewBox="0 0 73 40" aria-hidden="true"><use href="#zeichen"/></svg>
<span class="marke__text"><span class="marke__name">Marit Oswald</span><span class="marke__rolle">Interaction-Design<span class="breit">, Heidelberg</span></span></span>
</a>
<nav class="nav" aria-label="Hauptnavigation">
<span class="status mono">Frei ab Februar 2027</span>
<a href="#arbeiten">Arbeiten</a>
<a href="#notizen" class="weg">Notizen</a>
<a href="#kontakt">Kontakt</a>
</nav>
</div>
<a class="wortmarke" href="#oben" aria-label="Marit Oswald, zum Seitenanfang">Marit Oswald</a>
</header>
<main id="oben">
<section class="intro innen">
<div class="spalte">
<p class="mono">Heidelberg-Bergheim<span class="breit"> · selbstständig seit 2017</span></p>
<h1><span class="satz-a">Marit Oswald ist Interaction-Designerin in Heidelberg.</span><span class="satz-c">Interaction-Design aus Heidelberg.</span> Sie gestaltet Software für Menschen, die <em>jeden Tag</em> damit arbeiten: Laborsysteme, Dienstpläne, Ausleihterminals. Vorher sechs Jahre im Produktteam eines Medizintechnik-Herstellers in Mannheim.</h1>
</div>
</section>
<nav class="leiste" aria-label="Inhalt" data-leiste>
<div class="innen">
<div class="leiste__innen">
<div class="leiste__links">
<span class="leiste__inhalt mono">Inhalt</span>
<a class="leiste__name" href="#oben" aria-label="Marit Oswald, zum Seitenanfang"><svg viewBox="0 0 73 40" aria-hidden="true"><use href="#zeichen"/></svg>Marit Oswald</a>
</div>
<div class="nav">
<a href="#arbeiten">Arbeiten</a>
<a href="#notizen">Notizen</a>
<a href="#kontakt">Kontakt</a>
</div>
</div>
</div>
</nav>
<div class="innen">
<div class="spalte">
<section class="abschnitt" id="jetzt">
<h2>Jetzt</h2>
<p class="text">Bis Januar baue ich mit einer kleinen Rösterei aus der Weststadt ein Bestellsystem für Cafés: Wochenmengen, Mahlgrad, Lieferfenster. Danach ist Platz für ein neues Projekt, am liebsten eines, das <em>lange</em> halten soll.</p>
</section>
<section class="abschnitt" id="arbeiten">
<h2>Arbeiten</h2>
<ul class="liste">
<li><a href="#arbeiten"><span class="t">Probenbuch</span><span class="d">Laborsoftware für ein Analyselabor in Walldorf</span><span class="j">2026</span></a></li>
<li><a href="#arbeiten"><span class="t">Ausleihe am Terminal</span><span class="d">Selbstverbuchung für eine Stadtteilbibliothek</span><span class="j">2025</span></a></li>
<li><a href="#arbeiten"><span class="t">Taktgeber</span><span class="d">Dienstplan-App für einen ambulanten Pflegedienst</span><span class="j">2025</span></a></li>
<li><a href="#arbeiten"><span class="t">Fährzeiten</span><span class="d">Fahrplan und Ticket für eine Neckarfähre</span><span class="j">2024</span></a></li>
<li><a href="#arbeiten"><span class="t">Feldnotiz</span><span class="d">Kartierungs-App für eine ornithologische Arbeitsgruppe</span><span class="j">2022</span></a></li>
<li><a href="#arbeiten"><span class="t">Sprechstunde</span><span class="d">Terminvergabe für eine Hausarztpraxis in Rohrbach</span><span class="j">2021</span></a></li>
</ul>
</section>
<section class="abschnitt" id="stimme">
<h2>Stimme</h2>
<p class="zitat">„Nach drei Wochen hat niemand mehr nach dem alten System gefragt. Das ist bei uns im Labor das größte Lob, das es gibt.“</p>
<p class="mono">Laborleitung, Analyselabor Walldorf</p>
</section>
<section class="abschnitt" id="notizen">
<h2>Notizen</h2>
<ul class="liste">
<li><a href="#notizen"><span class="t">Fokusringe müssen nicht hässlich sein</span><span class="j">12.10.26</span></a></li>
<li><a href="#notizen"><span class="t">Tabellenziffern in Formularen</span><span class="j">03.08.26</span></a></li>
<li><a href="#notizen"><span class="t">Drei Sekunden Geduld: Ladezustände im Labor</span><span class="j">21.05.26</span></a></li>
<li><a href="#notizen"><span class="t">Warum ich Dienstpläne auf Papier skizziere</span><span class="j">14.02.26</span></a></li>
</ul>
</section>
<section class="abschnitt" id="kontakt">
<h2>Kontakt</h2>
<div class="kontakt">
<button class="kopieren" type="button" data-kopieren="post@marit-oswald.de"><span class="adresse">post@marit-oswald.de</span><span class="mono" data-hinweis>Kopieren</span></button>
<p class="text" style="color:var(--leise)">Termine dienstags und donnerstags, gern im Büro in der Bergheimer Straße oder per Video.</p>
</div>
</section>
<footer class="fuss"><span>© 2026 Marit Oswald</span><span>Impressum · Datenschutz</span></footer>
</div>
</div>
</main>
<aside class="steuer mono" aria-label="Demo-Steuerung">
<div class="modi" role="group" aria-label="Variante">
<button type="button" data-variante="schrumpfen" aria-pressed="true">Schrumpfen</button>
<button type="button" data-variante="andocken" aria-pressed="false">Andocken</button>
<button type="button" data-variante="wortmarke" aria-pressed="false">Wortmarke</button>
</div>
<div class="werte">
<span class="werte__regel" data-regel></span>
<span class="werte__live" data-live></span>
</div>
</aside>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const kopf = document.querySelector('[data-kopf]');
const leiste = document.querySelector('[data-leiste]');
const regel = document.querySelector('[data-regel]');
const live = document.querySelector('[data-live]');
const kannTimeline = CSS.supports('animation-timeline: scroll()');
const kannStuck = CSS.supports('container-type: scroll-state');
const telefon = matchMedia('(max-width: 640px)');
// Werte je Variante: Scrollweg, ab wann das Glas erscheint, Anzeige
const V = {
schrumpfen: { weg: 500, glasAb: 1, ziel: () => 760,
text: () => telefon.matches ? 'Höhe 120 → 56 px · Marke 4 → 3 rem · 0–500 px linear' : 'Höhe 168 → 64 px · Marke 4.5625 → 3.125 rem · 0–500 px linear' },
wortmarke: { weg: 150, glasAb: 1, ziel: () => 420,
text: () => telefon.matches ? 'Name 40 → 16 px · y 64 → 20 px · 0–150 px linear' : 'Name 64 → 16 px · y 112 → 24 px · 0–150 px linear' },
andocken: { weg: 0, glasAb: 0, ziel: () => leiste.offsetTop + 420,
text: () => (kannStuck ? 'scroll-state(stuck: top)' : 'Fallback: Klasse per Scroll-Abfrage') + ' · Glas 200 ms ease-in-out · blur 12 px' },
};
let variante = 'schrumpfen';
function lesen() {
const y = Math.max(0, scrollY), v = V[variante];
let p = 0, an = false;
if (variante === 'andocken') {
an = leiste.getBoundingClientRect().top <= 0.5 && y > 0;
leiste.classList.toggle('angedockt', !kannStuck && an);
} else {
if (reduziert()) p = y >= v.weg / 2 ? 1 : 0; // zwei feste Zustände
else if (!kannTimeline) p = Math.min(1, y / v.weg); // JS-Fallback, gleiche Rechnung
if (reduziert() || !kannTimeline) kopf.style.setProperty('--p', p);
else p = parseFloat(getComputedStyle(kopf).getPropertyValue('--p')) || 0;
an = y >= v.glasAb;
html.classList.toggle('angedockt', an);
}
live.innerHTML = variante === 'andocken'
? `<span>scrollY <b>${Math.round(y)}</b> px</span><span>stuck <b>${an ? 'ja' : 'nein'}</b></span>`
: `<span>scrollY <b>${Math.round(y)}</b> px</span><span>p <b>${p.toFixed(2)}</b></span><span>Glas <b>${an ? 'an' : 'aus'}</b></span>`;
}
function setzen(neu) {
variante = neu;
html.dataset.variante = neu;
html.classList.remove('angedockt');
leiste.classList.remove('angedockt');
document.querySelectorAll('.modi [data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === neu)));
regel.textContent = V[neu].text();
requestAnimationFrame(lesen);
}
document.querySelectorAll('.modi [data-variante]').forEach(b => b.addEventListener('click', () => { pause(); setzen(b.dataset.variante); scrollTo(0, 0); }));
let lesenGeplant = false;
addEventListener('scroll', () => { if (lesenGeplant) return; lesenGeplant = true; requestAnimationFrame(() => { lesenGeplant = false; lesen(); }); }, { passive: true });
addEventListener('resize', () => { regel.textContent = V[variante].text(); lesen(); });
// E-Mail kopieren, mit kurzem Zustand
const knopf = document.querySelector('[data-kopieren]'), hinweis = knopf.querySelector('[data-hinweis]');
let hinweisUhr = null;
knopf.addEventListener('click', () => {
const fertig = t => { hinweis.textContent = t; clearTimeout(hinweisUhr); hinweisUhr = setTimeout(() => { hinweis.textContent = 'Kopieren'; }, 1600); };
if (navigator.clipboard) navigator.clipboard.writeText(knopf.dataset.kopieren).then(() => fertig('Kopiert'), () => fertig('Bitte markieren'));
else fertig('Bitte markieren');
});
// Vorführung: Seite scrollt von selbst hinunter, hält, kehrt zurück; in der Vorschau wechselt die Variante je Runde
const RUNDE = 7600, FOLGE = ['schrumpfen', 'wortmarke', 'andocken'];
let uhr = null, t0 = 0, runde = 0;
const weich = q => 0.5 - 0.5 * Math.cos(Math.min(1, Math.max(0, q)) * Math.PI);
function vorfuehren() {
pause(); runde = 0;
if (html.classList.contains('vorschau')) setzen(FOLGE[0]);
scrollTo(0, 0);
t0 = performance.now();
html.classList.add('vorfuehrung');
const schritt = t => {
const dt = Math.max(0, t - t0), n = Math.floor(dt / RUNDE), p = (dt % RUNDE) / RUNDE;
if (n !== runde) { runde = n; if (html.classList.contains('vorschau')) setzen(FOLGE[n % FOLGE.length]); }
const ziel = Math.min(V[variante].ziel(), document.documentElement.scrollHeight - innerHeight);
let y;
if (p < 0.1) y = 0;
else if (p < 0.5) y = ziel * weich((p - 0.1) / 0.4);
else if (p < 0.66) y = ziel;
else if (p < 0.92) y = ziel * (1 - weich((p - 0.66) / 0.26));
else y = 0;
scrollTo(0, y);
uhr = requestAnimationFrame(schritt);
};
uhr = requestAnimationFrame(schritt);
}
function pause() { cancelAnimationFrame(uhr); uhr = null; html.classList.remove('vorfuehrung'); }
function neustart() { pause(); scrollTo(0, 0); setzen(variante); }
function weiter() { if (!uhr) vorfuehren(); }
setzen(variante);
})();
</script>
</body>
</html>