Scroll-driven Animations

Bei Scroll-driven Animations wird der Fortschritt einer CSS-Animation nicht durch Zeit, sondern durch die Scrollposition gesteuert. Die Animation kann dabei dem Scrollfortschritt der Seite oder der Position eines Elements im Viewport folgen.


Zentrale Begriffe

  • animation-timelineVerbindet den Fortschritt einer Animation mit einer Timeline.
  • scroll()Die Timeline folgt dem Scrollfortschritt eines Scroll-Containers.
  • view()Die Timeline folgt der Position eines Elements im Viewport.
  • animation-rangeBestimmt, welcher Abschnitt der Timeline für die Animation verwendet wird.

1. animation-timeline – Timeline zuweisen

animation-timeline bestimmt, welche Timeline den Fortschritt einer Animation steuert. Statt der verstrichenen Zeit kann die Scrollposition der Seite oder die Position eines Elements im Viewport verwendet werden.

animation-timeline: scroll(root);
animation-timeline: view();

animation-timeline wird nach der Kurzschreibweise animation notiert, weil diese den Wert sonst wieder auf den Standard zurücksetzt.

.element {
  animation: fadeUp linear both;
  animation-timeline: view();
}

2. scroll() – Scroll-Container wählen

scroll() koppelt den Fortschritt einer Animation an die Scrollposition eines Scroll-Containers. Ohne Angabe verwendet der Browser den nächsten scrollbaren Vorfahren und die Block-Achse.

  • scroll()Verwendet standardmässig den nächsten scrollbaren Vorfahren
  • scroll(root)Verwendet den Scrollfortschritt der gesamten Seite
  • scroll(nearest)Wählt ausdrücklich den nächsten scrollbaren Vorfahren
  • scroll(self)Verwendet die Scrollposition des Elements selbst
animation-timeline: scroll();
animation-timeline: scroll(root);
animation-timeline: scroll(nearest);
animation-timeline: scroll(self);

scroll() und scroll(nearest) bewirken dasselbe. scroll(self) funktioniert nur, wenn das Element selbst ein Scroll-Container ist.

scroll(root) eignet sich besonders für seitenweite Effekte wie Progress-Bars oder Parallax-Animationen. Für Animationen einzelner Elemente ist view() meist besser geeignet.

Scrolle in dieser Box

  • Das Hintergrundbild folgt dem Scrollfortschritt dieses Containers.
  • Je weiter du innerhalb der Box scrollst, desto weiter verschiebt sich das Bild.
  • Die Animation wird nicht durch Zeit gesteuert.
  • Der Scroll-Container steuert die Position seines eigenen Hintergrundbildes.
  • Am Ende des Scrollbereichs ist auch die Animation am Ende.

The quick brown fox jumps over the lazy dog
Der Satz ist so bekannt, weil es sich um ein Pangramm handelt – ein Satz, der jeden einzelnen Buchstaben des englischen Alphabets (von A bis Z) mindestens einmal enthält. Deshalb wird er seit Jahrzehnten genutzt, um Schriftarten zu testen oder das Tippen auf Tastaturen zu üben.

.scroll-demo {
  overflow-y: auto;
  background-position: right top;
  animation: move-background linear both;

  /* Das Element verwendet seinen eigenen Scrollfortschritt */
  animation-timeline: scroll(self);
}

@keyframes move-background {
  to {
    background-position: left bottom;
  }
}

3. view() – Element im Viewport

view() koppelt den Fortschritt der Animation an die Position eines Elements im Viewport. Die Animation verändert sich, während das Element hinein-, hindurch- oder hinausscrollt.

Meine Position im Viewport steuert die Animation.
.view-demo-box {
  animation: appear linear both;

  /* Die Position der Box im Viewport steuert die Animation */
  animation-timeline: view();

  /* Die Animation läuft vom Eintritt bis zur Mitte */
  animation-range: entry 0% cover 50%;
}

@keyframes appear {
  from {
    opacity: 0;
    transform: translateX(-200px);
  }

  to {
    opacity: 1;
    transform: translateX(0);
  }
}

4. animation-range – Bereich festlegen

animation-range bestimmt, welcher Abschnitt der Timeline den Keyframes von 0 % bis 100 % entspricht. So lässt sich festlegen, wann eine Animation beginnt und wann sie abgeschlossen ist.

Range-Werte

  • normalVerwendet den gesamten Standardbereich der Timeline.
  • 20% 80%Die Animation läuft zwischen 20 % und 80 % der Timeline.
  • entryDas Element kommt in den Viewport hinein.
  • exitDas Element verlässt den Viewport.
  • coverDas Element bewegt sich durch den sichtbaren Bereich.
  • containDas Element befindet sich vollständig im Viewport.

Syntax-Muster

animation-range: entry 20% cover 50%;

Die Angabe kombiniert einen Range-Namen mit einem Offset, meist in Prozent.


Aufbau einer Scroll-Animation

Eine Scroll-driven Animation verbindet klassische Keyframes mit einer scrollbasierten Timeline.

  • @keyframes definiert, was passiert
  • animation weist die Keyframes dem Element zu
  • animation-timeline bestimmt, wodurch der Fortschritt gesteuert wird
  • animation-range begrenzt optional den verwendeten Abschnitt der Timeline
@keyframes fadeUp {
    from {
        opacity: 0;
        transform: translateY(16px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.card {
    animation: fadeUp linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 50%;
}

In diesem Beispiel folgt die Animation der Position der Karte im Viewport. Sie beginnt, wenn die Karte in den Viewport eintritt, und ist in der Mitte des cover-Bereichs abgeschlossen.


Beispiele

1. Progress Bar

HTML

<div class="progress"></div>

CSS - Look

.progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  transform-origin: left;
  transform: scaleX(0);
  background: linear-gradient(45deg, cyan, yellow, red, indigo, cyan);
  z-index: 9999;
}

CSS - Animation

@supports (animation-timeline: scroll(root)) {
  .progress{
    animation: grow linear both;
    animation-timeline: scroll(root);
  }
  @keyframes grow{
    to { transform: scaleX(1); }
  }
}

Bewegung reduzieren

@media (prefers-reduced-motion: reduce) {
  .progress {
    display: none;
  }
}

Erklärung: Oben ist eine Linie, die von 0 → 100% wächst, während du scrollst.
Tipp: Für Progress-Bars ist transform: scaleX() sehr performant (GPU-freundlich).


2. Pin-Animationen (scroll(root))

In dieser Demo bleibt jede Box kurz „gepinnt“ (via position: sticky). Während du weiter scrollst, scrubbt die Animation entlang der Seiten-Scrollposition (scroll(root)).

See the Pen Scroll-Timeline by Intensivstation (@intensivstation) on CodePen.

Erklärung

  • Alle Boxen hängen an derselben Scroll-Timeline (scroll(root)).
  • Der Scroll ersetzt die Zeit: 0% → 100% entspricht einer Scroll-Strecke.
  • Der sichtbare Unterschied entsteht nur durch unterschiedliche @keyframes.

Kernidee

.pin { position: sticky; top: 20px; }

@supports (animation-timeline: scroll(root)) {
  .box{
    animation: effect linear both;
    animation-timeline: scroll(root);
    animation-range: 0% 25%;
  }
}

Merksatz

  • scroll(root) = Timeline folgt der Scrollposition der Seite.
  • view() = Timeline folgt der Sichtbarkeit eines Elements.

3. View-basierte Reveal-Animation

Bei view() wird der Fortschritt der Animation an die Sichtbarkeit eines Elements im Viewport gekoppelt. Je weiter das Element eintritt, desto weiter läuft die Animation.

Reveal 1

Startet beim Reinkommen.

Reveal 2

Die Position der Karte steuert die Animation.

Reveal 3

Sichtbarkeit steuert die Animation.

Erklärung

  • Die Animation startet, wenn das Element in den Viewport kommt.
  • view() koppelt die Animation an Sichtbarkeit.
  • Ausserhalb des Viewports steht die Animation still.

Kernidee

@supports (animation-timeline: view()) {
  .reveal {
    opacity: 0;
    transform: translateY(60px) scale(.9);
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 50%;
  }

  @keyframes reveal {
    to {
      opacity: 1;
      transform: translateY(0) scale(1);
    }
  }
}

Hinweis: Visuell kann view() ähnlich wirken wie scroll(root). Der Unterschied zeigt sich im Verhalten: view() reagiert auf Sichtbarkeit, scroll(root) auf die Seiten-Scrollposition.


4. Animation-range vergleichen

animation-range legt fest, welcher Abschnitt einer Timeline für 0% → 100% der Animation verwendet wird. Hier siehst du den gleichen Effekt, aber mit zwei unterschiedlichen Ranges.

So testest du es: Scroll langsam und achte darauf, wann die Boxen beginnen und wann sie „fertig“ sind.

Variante A · Prozent
20% → 80%

Startet erst bei 20% der Timeline und ist bei 80% fertig.

Variante B · Keywords
entry 20% → cover 50%

Bezieht sich auf Sichtbarkeits-Phasen des Elements (entry/cover).

Erklärung

  • Prozent (20% → 80%) bezieht sich direkt auf die Timeline (0–100%).
  • Keywords (entry…cover…) beziehen sich auf Phasen der Sichtbarkeit im Viewport.
  • Wenn ein Effekt „zu früh“ startet oder „zu spät“ endet: animation-range ist fast immer der Hebel.

Kernidee

@supports (animation-timeline: view()) {
  .rangeBox { 
    animation: pop linear both;
    animation-timeline: view();  
}

  .rangeBox[data-range="percent"]  { animation-range: 20% 80%; }
  .rangeBox[data-range="keywords"] { animation-range: entry 20% cover 50%; }
}

5. Bento Grid mit Scroll-Animation

Dieses Beispiel kombiniert view(), animation-range, Custom Properties und ein responsives Bento Grid. Bento Grid mit Scroll-Animation auf CodePen ansehen.


6. Elemente staffeln

Sollen mehrere Elemente nicht gleichzeitig, sondern nacheinander erscheinen – z. B. Listeneinträge, Karten oder Galerie-Bilder – reicht eine einzige @keyframes-Regel für alle. Die Staffelung entsteht über ein versetztes animation-delay oder einen Range-Offset pro Element.

Klassisch: :nth-child()

Jedes Element bekommt über :nth-child() (siehe Pseudoklassen) ein eigenes Delay. Funktioniert überall, muss aber für jede Position einzeln geschrieben werden. Ein Beispiel für reines Staffeln beim Laden, ohne Scroll-Trigger, gibt es in dieser Animated Gallery auf CodePen.

1
2
3
4

Diese Demo startet erst, wenn du bis hierhin scrollst – nicht beim Laden der Seite. Sie kombiniert zwei Techniken: den Range-Offset pro Element für die Staffelung (:nth-child()) mit einer scrollbasierten Timeline, die den Start an die Sichtbarkeit koppelt statt an die Ladezeit (animation-timeline: view()).

Hinweis: Lädst du die Seite neu, während dieser Abschnitt schon im Sichtbereich ist, spielt die Animation nicht nochmal ab – view() zeigt immer den Zustand, der zur aktuellen Scrollposition passt, statt jedes Mal von vorne zu starten. Kein Bug, das ist der Unterschied zu einer zeitbasierten Animation.

1. Animation definieren

@keyframes staggerIn {
  from {
    opacity: 0;
    transform: translateY(70px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

2. Animation zuweisen

.stagger-demo .samp {
  animation: staggerIn 0.6s ease forwards;
}

3. Staffelung über Sekunden-Delay

/* Fallback ohne view()-Support: Staffelung über Sekunden-Delay */

@supports not (animation-timeline: view()) {
  .stagger-demo .samp:nth-child(1) { animation-delay: 0s; }
  .stagger-demo .samp:nth-child(2) { animation-delay: 0.25s; }
  .stagger-demo .samp:nth-child(3) { animation-delay: 0.5s; }
  .stagger-demo .samp:nth-child(4) { animation-delay: 0.75s; }
}

4. Staffelung über Range-Offset

/* Start an Sichtbarkeit statt an Ladezeit koppeln, Staffelung über Range-Offset */

@supports (animation-timeline: view()) {
.stagger-demo .samp {
    animation-timeline: view();
    animation-range: entry 0% cover 40%;
  }

.stagger-demo .samp:nth-child(2) { animation-range: entry 5% cover 60%; }
.stagger-demo .samp:nth-child(3) { animation-range: entry 10% cover 80%; }
.stagger-demo .samp:nth-child(4) { animation-range: entry 15% cover 100%; }
}

sibling-index() – Bleeding Edge

Statt jede Position manuell zu adressieren, liefert sibling-index() die Position eines Elements unter seinen Geschwistern direkt als Zahl – nutzbar in calc(). Eine einzige Regel reicht für beliebig viele Elemente, auch wenn später mehr dazukommen.

.stagger-demo .samp {
  animation: staggerIn 0.6s ease forwards;
  animation-delay: calc((sibling-index() - 1) * 0.15s);
}

Browser-Support (Stand 2026): sibling-index() ist seit August 2026 Baseline und funktioniert in den aktuellen Browsern.


Fallback

Progressive Enhancement

Scroll-Timelines sind modern. Deshalb: Feature testen und nur dann aktivieren.

@supports (animation-timeline: view()) {
  /* moderne scroll-driven Version */
}

@supports not (animation-timeline: view()) {
  /* Fallback: statische Darstellung oder normale Transition */
}

Reduzierte Bewegung

prefers-reduced-motion reagiert auf die Einstellungen der Nutzer:innen und ist wichtig für barrierearme Animationen. Scroll-Animationen sollten die Einstellung prefers-reduced-motion berücksichtigen.

@media (prefers-reduced-motion: reduce) {
  .pop {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .progress {
    display: none;
  }
}

Performance

Für flüssige Scroll-Animationen eignen sich besonders transform und opacity. Diese Eigenschaften verändern den Dokumentenfluss nicht und können vom Browser meist effizient dargestellt werden.

Animationen von Layout-Eigenschaften wie top, left, width oder height können aufwendiger sein. Scroll-Effekte sollten zudem zurückhaltend eingesetzt werden, damit die Seite ruhig und gut bedienbar bleibt.


Kurz gesagt

  • Scroll-driven Animations werden durch Scrollen statt durch Zeit gesteuert.
  • scroll(root) folgt der Scrollposition der Seite.
  • view() folgt der Sichtbarkeit eines Elements.
  • animation-range bestimmt Start und Ende innerhalb der Timeline.
  • @supports und prefers-reduced-motion sorgen für Fallback und reduzierte Bewegung.

NurseVerwandte Themen

  • Animation – Abläufe mit Keyframes und Animation-Eigenschaften gestalten.
  • Transform – Elemente verschieben, drehen, skalieren oder neigen.
  • Transition – Zustandsänderungen fliessend darstellen.
  • CSS Scroll-Properties – Scrollverhalten, Snap-Punkte und Abstände steuern.