1 / 4
Sticky hält die Karte.
top: 5.5rem, mehr braucht es nicht, damit die Karte hängen bleibt, bis der Stapel zu Ende ist.
Showcase 59
Diese Seite enthält kein einziges Skript. Alles, was du siehst, ist CSS und HTML. Scroll-Animationen, Tooltips, Tabs, ein Farbwähler, eine Karte, die du selbst breiter ziehen kannst.
Dein Browser kann animation-timeline: scroll() noch nicht. Der Zähler und der Fortschrittsbalken fehlen deshalb, der Rest der Seite funktioniert.
Du hast reduzierte Bewegung eingestellt. Alle Scroll-Animationen sind aus, jeder Zustand ist direkt sichtbar.
Scrollen, der Rest erklärt sich
@property + animation-timeline: scroll(root)@property --n { syntax: "<integer>"; initial-value: 0; inherits: false }
.pct { animation: count linear both; animation-timeline: scroll(root); counter-reset: n var(--n) }
.pct::after { content: counter(n) " %" }01 · Scroll-getriebene Animationen
Eine Animation muss nicht an der Uhr hängen. Mit animation-timeline: view() läuft sie genau so weit, wie das Element im Sichtfenster ist. Kein IntersectionObserver, kein Schwellwert, kein Skript.
Dein Browser kann view()-Timelines noch nicht. Die Karten stehen deshalb einfach da, und das Bild bleibt still.
animation-timeline: view() + animation-range.card { animation: fly-in linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%; }02 · Sticky-Stack
Vier Abschnitte kleben per position: sticky oben fest. Jede Karte schrumpft und dunkelt ab, sobald die nächste hereinkommt. Was hier hereinkommt, weiß die Karte über die Timeline ihres Nachfolgers.
Ohne Scroll-Timelines stapeln sich die Karten trotzdem, sie schrumpfen nur nicht.
1 / 4
top: 5.5rem, mehr braucht es nicht, damit die Karte hängen bleibt, bis der Stapel zu Ende ist.
2 / 4
Normalerweise sehen nur Nachfahren eine benannte Timeline. Mit timeline-scope auf dem Stapel darf Karte 1 die Timeline von Karte 2 abhören.
3 / 4
Während die nächste Karte von unten hereinfährt, läuft auf der vorherigen die Animation von Skala 1 auf 0,9.
4 / 4
Kein Nachfolger, keine Timeline, keine Animation. Der Effekt, den man von Apple-Produktseiten kennt, in 30 Zeilen CSS.
position: sticky + timeline-scope + view-timeline-name.stack { timeline-scope: --c1, --c2, --c3, --c4 }
.card:nth-child(2) { view-timeline-name: --c2 }
.card:nth-child(1) { animation-timeline: --c2; animation-range: entry 0% entry 100% }03 · Anchor Positioning & Popover
Ein Tooltip hängt an seinem Knopf wie an einem Anker. Passt er oben nicht mehr hin, klappt er nach unten. Das Popover öffnet sich mit dem popover-Attribut und schließt sich per Escape oder Klick daneben, eingebaut im Browser.
Dein Browser kann Anchor Positioning noch nicht. Die Tooltips sitzen deshalb klassisch absolut über dem Knopf, das Popover in der Bildschirmmitte.
Dein Browser kann @starting-style noch nicht, deshalb erscheinen Tooltip und Popover ohne Einblendung.
position-area: top. Scroll den Knopf an den oberen Rand, dann klappe ich nach unten.position-area: right, mit flip-inline als Ausweg am rechten Rand.Top Layer, Light-Dismiss, Fokusverwaltung, alles vom Browser. Es hängt unter dem Knopf, weil der Knopf ein anchor-name trägt. Steht der Knopf am unteren Rand, klappt es nach oben.
anchor-name / position-anchor / position-area / @starting-style.btn { anchor-name: --pop }
[popover] { position-anchor: --pop; position-area: bottom span-right; position-try-fallbacks: flip-block }
@starting-style { [popover]:popover-open { opacity: 0 } }04 · Text & Container
Überschriften auf dieser Seite sind mit text-wrap: balance gesetzt, Absätze mit pretty. Der Browser sucht den Umbruch, der keine einzelnen Wörter auf der letzten Zeile zurücklässt.
Drop Caps waren lange ein Fall für Floats mit handgemessenem Zeilenabstand. initial-letter: 3 2 sagt stattdessen, was gemeint ist: drei Zeilen hoch, zwei Zeilen tief versenkt. Der Browser rechnet die Grundlinie selbst aus, unabhängig von der Schrift. Ändert sich die Schriftgröße, bleibt der Buchstabe im Lot.
Dieser Absatz brennt sich Zeile für Zeile ein, während er durch das Sichtfenster wandert. Ein Verlauf, in Text geschnitten, gesteuert von einer Prozentzahl, die am Scrollbalken hängt.
Ohne Scroll-Timeline steht der Absatz oben komplett eingefärbt da.
Unten rechts sitzt ein Griff (resize: horizontal). Ab 30rem Breite wechselt die Karte per Container-Query ins Zwei-Spalten-Layout. Auf Touch-Geräten gibt es den Griff nicht.
Schmal · gestapeltBreit · zwei Spalten
Die Karte fragt nicht das Fenster, sondern ihren eigenen Container.
Dieser Satz existiert nur ab 30rem. Er ist nicht ausgeblendet, er wurde eingeblendet.
@container + resize + initial-letter + text-wrap.box { container-type: inline-size; resize: horizontal; overflow: auto }
@container (min-width: 30rem) { .cq { grid-template-columns: 8rem 1fr } }
p::first-letter { initial-letter: 3 2 }05 · :has()-Zustandsmaschine
Unsichtbare Radios und Checkboxen halten den Zustand. Der Rest der Seite liest ihn per :has(:checked). Das ist keine Spielerei, das ist ein vollständiger Zustandsautomat, den der Browser mitbringt.
Hintergrund, Schrift, Farbe, Rundung und Abstand kommen aus Custom Properties. Welche gelten, entscheidet ein Selektor weiter oben im Baum, der nach den Radios schaut.
Drei Radios mit demselben name, drei Labels, drei Panels. Der Container prüft, welches Radio gewählt ist, und zeigt das passende Panel.
Radios sind von Haus aus per Pfeiltaste wechselbar. Der Fokus bleibt sichtbar, weil das Label auf :focus-visible des Radios reagiert.
Was CSS nicht kann: den Zustand in die URL schreiben, ihn speichern oder auf einen Server schicken. Wer das braucht, braucht ein Skript, und sollte es dann auch nehmen.
Alle drei details tragen denselben name. Öffnet sich eins, schließt der Browser die anderen. Ohne Attribut wären es drei unabhängige Klappen.
details und summary sind native Elemente mit Rolle, Tastaturbedienung und Zustand. Besser wird es mit eigenem Markup selten.
Dafür gibt es ::details-content und interpolate-size. Beides ist neuer als der Rest hier, deshalb bleibt es auf dieser Seite beim harten Umschalten.
:has(:checked) + <details name>.state:has(#ink:checked) .panel { --bg: var(--ink); --fg: var(--paper) }
.tabs:has(#tab-2:checked) [data-tab="2"] { display: block }
<details name="acc"> …exklusive Gruppe, nativ…06 · Farben
Der Farbton ist eine registrierte Custom Property mit Syntax <integer>. Wählst du unten einen anderen, wechselt die ganze Seite: Akzent, Verläufe, Buttons, das Parallax-Bild oben. Und weil die Property typisiert ist, kann sie sogar weich übergehen.
Das letzte Feld nutzt light-dark(): eine Farbe für hell, eine für dunkel, der Browser nimmt die zur Systemeinstellung passende. Die Seite folgt deinem System, nicht dem Rest der Website. Das fett und das kursiv in diesem Abschnitt sind per @scope eingefärbt, der Code-Block unten bleibt davon unberührt, weil er das Ende des Scopes ist.
@property + oklch() + color-mix() + light-dark() + @scope.page:has(#hue-3:checked) { --hue: 150 }
--acc: oklch(58% 0.19 var(--hue)); --tint: color-mix(in oklch, var(--acc) 20%, white);
@scope (.palette) to (pre) { strong { color: var(--acc) } }07 · Was das bedeutet
Jede Zeile JavaScript, die du nicht ausliefern musst, kann nicht langsam laden, nicht bei der Hydration stolpern und nicht im falschen Moment einen Listener vergessen. Scroll-Animationen laufen im Compositor, nicht im Main Thread. Tooltips und Popover bekommen Fokusverwaltung und Escape gratis.
Die Rechnung hat eine Rückseite: Browser-Support. Ein Teil dieser Seite läuft heute nur in Chromium und Safari. Firefox zieht bei Scroll-Timelines und Anchor Positioning nach, hat es aber Stand heute noch hinter einer Einstellung. Deshalb steht bei jedem Abschnitt ein @supports-Fallback, der ehrlich sagt, was fehlt, statt kaputt zu wirken. Genau das ist die Arbeit: nicht das Feature, sondern der Weg ohne das Feature.
| Feature | Chrome / Edge | Firefox | Safari |
|---|---|---|---|
| animation-timeline: scroll() / view() | ja seit 115 | teils hinter Flag | ja seit 26 |
| @property | ja seit 85 | ja seit 128 | ja seit 16.4 |
| anchor-name / position-area | ja seit 125 | teils hinter Flag | ja seit 26 |
| popover-Attribut | ja seit 114 | ja seit 125 | ja seit 17 |
| @starting-style + allow-discrete | ja seit 117 | ja seit 129 | ja seit 17.5 |
| text-wrap: balance / pretty | ja 114 / 117 | teils balance ja, pretty spaet | ja 17.5 / 26 |
| initial-letter | ja seit 110 | nein Fallback: float | ja seit 9 (Prefix) |
| @container + resize | ja seit 105 | ja seit 110 | ja seit 16 |
| :has() | ja seit 105 | ja seit 121 | ja seit 15.4 |
| <details name="…"> | ja seit 120 | ja seit 130 | ja seit 17.2 |
| @scope | ja seit 118 | ja seit 143 | ja seit 17.4 |
| oklch() / color-mix() / light-dark() | ja seit 123 | ja seit 120 | ja seit 17.5 |
Stand: Kenntnisstand des Autors, September 2026. Verbindlich ist caniuse.com.
Diese Seite: 1 Server Component, 1 CSS-Datei, 0 Skripte. Die Navigation oben gehört zur Website und zählt nicht mit.