CSS: Bewegung
CSS-Animationen: transition und keyframes
transition blendet einen Zustandswechsel weich aus, animation mit @keyframes beschreibt einen eigenen Ablauf mit beliebig vielen Zwischenschritten. Flüssig laufen beide über transform und opacity, und prefers-reduced-motion gehört zu jeder Animation dazu.
Bewegung auf einer Website hat zwei sinnvolle Aufgaben. Sie bestätigt eine Aktion, und sie erklärt, woher ein neues Element kommt. Alles darüber hinaus kostet Rechenzeit und stört einen Teil der Nutzenden körperlich. Jede Animation beginnt deshalb mit der Frage, welche Information die Bewegung überbringt.
Beide Werkzeuge gehören in dieselbe Datei wie der Rest deiner eingebundenen Stile. Dieser Text zeigt sie mit lauffähigen Beispielen, dazu die Eigenschaften, die der Browser flüssig darstellt, und den Medienblock, ohne den keine Animation ausgeliefert wird.
Das Wichtigste in Kürze
-
transitionbraucht einen Auslöser. Ein:hover, ein:focus-visibleoder eine neue Klasse aus JavaScript setzt den Zielwert. Den Weg dorthin rechnet der Browser aus. -
animationbraucht keinen Auslöser. Der@keyframes-Block beschreibt den Ablauf mitfrom,tooder Prozentwerten. Die Kurzformanimationstartet ihn. -
Flüssig laufen
transformundopacity. Beide ändern das Layout nicht.width,height,marginundtopzwingen den Browser bei jedem Bild zu einer neuen Layoutberechnung. -
@media (prefers-reduced-motion: reduce)ist Pflicht. Die Systemeinstellung „Bewegung reduzieren“ meldet ein gesundheitliches Bedürfnis. Wer sie übergeht, verursacht bei vestibulären Störungen Schwindel und Übelkeit. - Bewegung, die automatisch startet und länger als fünf Sekunden läuft, braucht einen Schalter. Das verlangt WCAG 2.2.2 auf Stufe A.
-
animation-delaywirkt nur vor dem ersten Durchlauf. Eine Pause zwischen zwei Wiederholungen entsteht über Prozentschritte, die den Ruhezustand halten.
transition oder animation: der Unterschied
transition beschreibt, wie ein Element von einem Zustand in einen anderen kommt. Es gibt zwei Werte, den alten und den neuen. Der Browser füllt die Zeit dazwischen. animation beschreibt einen Ablauf, der für sich steht. Er hat Anfang, Ende und beliebig viele Stationen dazwischen, startet von allein und kann sich wiederholen.
Die Faustregel lautet: Reagiert die Bewegung auf eine Handlung, nimm transition. Läuft sie von selbst oder braucht sie mehr als zwei Stationen, nimm animation.
transition: den Zustandswechsel weich machen
Die Eigenschaft steht am Ruhezustand des Elements. Dort gilt sie für beide Richtungen. Steht sie nur unter :hover, blendet der Hinweg weich aus und der Rückweg springt.
.karte {
transform: translateY(0);
transition-property: transform;
transition-duration: 200ms;
transition-timing-function: ease-out;
transition-delay: 0ms;
}
.karte:hover,
.karte:focus-within {
transform: translateY(-4px);
}
Die Auswahl der Selektoren und ihre Spezifität behandelt CSS-Selektoren.
Die vier Einzel-Eigenschaften
transition-property nennt die Eigenschaften, die überblendet werden, mehrere davon mit Komma getrennt. Der Wert all ist riskant, weil er auch Eigenschaften erfasst, die eine Layoutberechnung auslösen.
transition-duration legt die Dauer fest. Für Rückmeldungen auf einen Klick sind 100 bis 200 Millisekunden angemessen. Für größere Flächen gelten 200 bis 400 Millisekunden.
transition-delay wartet vor dem Start und hilft beim Ausblenden von Menüs, damit der Zeiger eine Lücke überqueren kann. Ein Wert über 100 Millisekunden wirkt träge.
timing-function: ease, linear und eigene Kurven
transition-timing-function bestimmt die Geschwindigkeit innerhalb der Dauer. Der Vorgabewert ease startet zügig und endet weich. ease-out wirkt bei Einblendungen am ruhigsten. linear hält das Tempo konstant und passt zu Drehungen. Eigene Kurven schreibst du mit cubic-bezier().
.beispiel-a { transition-timing-function: ease-out; }
.beispiel-b { transition-timing-function: linear; }
.beispiel-c { transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
.beispiel-d { transition-timing-function: steps(8, end); } /* ruckend, für Sprites */
Die Kurzform transition
Die Kurzform nimmt die Werte in der Reihenfolge Eigenschaft, Dauer, Kurve und Verzögerung.
/* Don't: all erfasst auch Eigenschaften, die eine Layoutberechnung auslösen */
.karte {
transition: all 300ms;
}
/* Do: die beiden Eigenschaften benennen, die der Compositor übernimmt */
.karte {
transition:
transform 200ms ease-out,
opacity 200ms ease-out;
}
CSS-Animation mit @keyframes
Ein @keyframes-Block bekommt einen Namen und beschreibt Stationen. Für zwei Stationen genügen from und to.
@keyframes einblenden {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hinweis {
animation: einblenden 400ms ease-out;
}
Für mehr als zwei Stationen stehen Prozentwerte. Mehrere dürfen sich einen Block teilen, wenn sie denselben Zustand halten.
@keyframes aufmerksam {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-6px); }
75% { transform: translateX(6px); }
}
Fehlt from oder 0%, nimmt der Browser den berechneten Wert des Elements als Startpunkt. Deshalb kommt ein Spinner mit einem einzigen to-Block aus.
Die animation-Eigenschaften im Überblick
Die Kurzform animation fasst neun Einzel-Eigenschaften zusammen.
| Eigenschaft | Wirkung | Beispielwert |
|---|---|---|
animation-name |
Verweist auf den @keyframes-Block |
einblenden |
animation-duration |
Legt die Dauer eines Durchlaufs fest | 600ms |
animation-timing-function |
Bestimmt die Geschwindigkeitskurve im Durchlauf | ease-out |
animation-delay |
Wartet vor dem ersten Durchlauf | 200ms |
animation-iteration-count |
Zählt die Durchläufe. infinite läuft endlos |
infinite |
animation-direction |
Kehrt die Laufrichtung jedes zweiten Durchlaufs um | alternate |
animation-fill-mode |
Hält den Zustand vor dem Start und nach dem Ende | forwards |
animation-play-state |
Hält den Ablauf an oder lässt ihn laufen | paused |
animation-timeline |
Ersetzt die Zeitachse durch den Scrollfortschritt | scroll() |
Zwei davon werden häufig übersehen. animation-fill-mode: forwards lässt das Element im Endzustand stehen, statt in den Ausgangszustand zurückzuspringen. animation-play-state: paused hält den Ablauf dort an, wo er gerade steht, und eignet sich für einen Pause-Schalter per Klasse.
Die Kurzform animation
.spinner {
animation: drehen 900ms linear 0ms infinite normal none running;
}
/* dieselbe Regel, kurz und im Alltag üblich */
.spinner {
animation: drehen 900ms linear infinite;
}
Die erste Zeitangabe ist die Dauer, die zweite die Verzögerung. Was du weglässt, setzt der Browser auf den Vorgabewert zurück. Eine spätere Einzelregel wie animation-play-state gehört deshalb hinter die Kurzform.
Endlos-Animationen: der Loop mit infinite
animation-iteration-count: infinite lässt den Ablauf laufen, bis das Element verschwindet oder die Eigenschaft entfällt. Ein Loop ist der Fall, bei dem WCAG 2.2.2 greift, sobald er länger als fünf Sekunden läuft und neben anderem Inhalt steht.
.pulsierender-punkt {
animation: pulsieren 1600ms ease-in-out infinite;
}
@keyframes pulsieren {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
Eine Pause zwischen zwei Durchläufen
animation-delay wartet einmalig vor dem ersten Durchlauf. Für eine Pause zwischen den Wiederholungen gibt es keine eigene Eigenschaft. Der übliche Weg verlängert die Dauer und hält den Ruhezustand über Prozentschritte.
/* 4s Gesamtdauer: 1,2s Bewegung, danach 2,8s Ruhe */
@keyframes winken {
0%, 30%, 100% { transform: rotate(0deg); }
10% { transform: rotate(14deg); }
20% { transform: rotate(-8deg); }
}
.winkende-hand {
animation: winken 4s ease-in-out infinite;
}
Die Bewegung endet bei 30 Prozent von 4 Sekunden. Den Rest hält der Block bei 100 Prozent auf demselben Wert, und daraus entsteht die Pause.
Was flüssig läuft: transform und opacity
Der Browser berechnet zuerst die Geometrie (Layout), malt dann die Pixel (Paint) und setzt am Ende die fertigen Ebenen zusammen (Composite). transform und opacity greifen erst im letzten Schritt an. Der Compositor verschiebt eine bereits gemalte Ebene und schafft dabei auch auf schwachen Geräten 60 Bilder pro Sekunde.
Verfügbar sind translate, scale und rotate, dazu skew. Die ersten drei gibt es auch als eigenständige Eigenschaften.
.beispiel {
transform: translateX(20px) scale(1.05) rotate(3deg);
}
/* alternativ als Einzel-Eigenschaften, in beliebiger Reihenfolge kombinierbar */
.beispiel-neu {
translate: 20px 0;
scale: 1.05;
rotate: 3deg;
}
Warum width, height und margin ruckeln
Ändert eine Animation width, height, margin, top oder padding, muss der Browser für jedes einzelne Bild das Layout neu berechnen. In einem Flexbox- oder Grid-Container betrifft das alle Geschwister, weil sich die Verteilung des freien Raums ändert. Pro Bild bleiben rund 16 Millisekunden, und eine Layoutberechnung über viele Elemente sprengt dieses Budget regelmäßig. Sichtbar wird das als Ruckeln. Messbar wird es als schlechter INP-Wert in den Core Web Vitals.
/* Don't: jedes Bild erzwingt eine neue Layoutberechnung */
@keyframes balken-langsam {
from { width: 0; }
to { width: 100%; }
}
/* Do: derselbe Effekt über den Compositor */
@keyframes balken {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.balken {
width: 100%;
transform-origin: left center;
animation: balken 800ms linear forwards;
}
scaleX staucht auch den Inhalt des Elements. Für eine leere Fläche wie einen Fortschrittsbalken passt das. Für Text nimmst du translate oder eine Maske. Weitere Stellschrauben stehen unter LCP und INP optimieren.
will-change sparsam einsetzen
will-change: transform kündigt dem Browser an, dass sich eine Eigenschaft ändern wird. Er legt daraufhin eine eigene Compositor-Ebene an. Jede Ebene belegt Speicher, und bei vielen Elementen kippt der Nutzen auf Mobilgeräten ins Gegenteil.
/* Don't: dauerhaft und pauschal auf viele Elemente */
* { will-change: transform; }
/* Do: nur dort, wo die Bewegung unmittelbar bevorsteht */
.karte:hover { will-change: transform; }
Setze will-change erst kurz vor der Bewegung und nimm es danach zurück. Ohne konkreten Ruckler brauchst du die Eigenschaft nicht.
Drei Beispiele aus der Praxis
Sanfter Hover-Übergang
Der Übergang gilt für Maus und Tastatur. :focus-visible steht deshalb neben :hover.
<button type="button" class="aktion">Termin buchen</button>
.aktion {
background-color: #14406b;
color: #fff;
transform: translateY(0);
transition:
transform 160ms ease-out,
background-color 160ms ease-out;
}
.aktion:hover,
.aktion:focus-visible {
transform: translateY(-2px);
background-color: #0d2c4b;
}
.aktion:active {
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.aktion { transition-duration: 1ms; }
}
Beide Farbwerte halten das Kontrastverhältnis von 4,5:1 zu Weiß ein. Das verlangt Kontrast (Minimum) nach 1.4.3. Wenn der Hover zusätzlichen Inhalt einblendet, gelten die Regeln aus 1.4.13 Inhalt bei Hover oder Fokus.
Lade-Spinner mit rotate
Eine Drehung um 360 Grad braucht nur einen to-Block. linear verhindert, dass die Drehung stockend wirkt.
<div class="ladezustand" role="status">
<span class="spinner" aria-hidden="true"></span>
<span class="ladezustand__text">Ergebnisse werden geladen</span>
</div>
.spinner {
display: inline-block;
inline-size: 1.5rem;
block-size: 1.5rem;
border: 3px solid #c9d4e0;
border-block-start-color: #14406b;
border-radius: 50%;
animation: drehen 900ms linear infinite;
}
@keyframes drehen {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: pulsieren 1600ms ease-in-out infinite;
}
}
Der Spinner sagt nichts an und bekommt deshalb aria-hidden="true". Die Information steckt im Text daneben. Der Container mit role="status" steht im DOM, bevor der Text hineingeschrieben wird, sonst kommt keine Ansage an. Die Details stehen unter
Status, Fortschritt und Benachrichtigungen
und im Kriterium 4.1.3 Statusmeldungen. Zentriert wird er wie unter Elemente zentrieren beschrieben.
Einblenden beim Erscheinen
Der Startzustand kommt aus einer Klasse, die JavaScript am <html>-Element setzt. Ohne JavaScript bleibt der Inhalt sichtbar.
.js .einblenden {
opacity: 0;
transform: translateY(16px);
transition:
opacity 400ms ease-out,
transform 400ms ease-out;
}
.js .einblenden.sichtbar {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.js .einblenden,
.js .einblenden.sichtbar {
opacity: 1;
transform: none;
transition: none;
}
}
document.documentElement.classList.add('js');
const beobachter = new IntersectionObserver((eintraege) => {
for (const eintrag of eintraege) {
if (eintrag.isIntersecting) {
eintrag.target.classList.add('sichtbar');
beobachter.unobserve(eintrag.target);
}
}
}, { threshold: 0.2 });
document.querySelectorAll('.einblenden')
.forEach((element) => beobachter.observe(element));
Der IntersectionObserver meldet jedes Element einmal und hört danach auf zu beobachten. Elemente mit opacity: 0 bleiben für Screenreader lesbar und für die Tastatur erreichbar. Soll der Inhalt vorher verborgen sein, nutze hidden oder ein details-Element.
Scroll-getriebene Animationen mit animation-timeline
animation-timeline ersetzt die Zeit als Fortschrittsquelle. Mit scroll() steuert der Scrollstand des Dokuments den Ablauf, mit view() die Position eines Elements im Sichtfeld. Eine Leseleiste am Seitenkopf kommt damit ohne JavaScript aus.
@supports (animation-timeline: scroll()) {
.leseleiste {
transform-origin: left center;
animation: balken linear;
animation-timeline: scroll(root block);
}
}
@media (prefers-reduced-motion: reduce) {
.leseleiste { animation: none; }
}
Stand September 2026 setzen die Chromium-Browser das um. In Safari und Firefox ist die Unterstützung nicht überall gegeben. Der @supports-Block hält die Seite ohne diese Funktion vollständig bedienbar. Baue scroll-getriebene Effekte als Ergänzung, die ersatzlos entfallen darf.
Barrierefreiheit: worauf es ankommt
Bewegung ist die eine Gestaltungsentscheidung, die Menschen körperlich beeinträchtigen kann. Bei vestibulären Störungen lösen große, schnelle oder unerwartete Bewegungen Schwindel, Übelkeit und Kopfschmerzen aus, und die Beschwerden halten oft Stunden an. Betroffen sind mehrere Prozent der erwachsenen Bevölkerung, und die Zahl steigt mit dem Alter. Deshalb steht dieser Abschnitt hier.
Der Reduce-Motion-Block gehört in jedes Projekt
Die Systemeinstellung „Bewegung reduzieren“ gibt es unter macOS, iOS, Windows und Android. Der Browser meldet sie über prefers-reduced-motion. Dieser Block gehört ans Ende deines Stylesheets:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
}
Die Dauer steht bei 1 Millisekunde statt bei 0. So feuern transitionend und animationend
weiterhin, und JavaScript, das darauf wartet, läuft weiter. Der umgekehrte Weg ist sauberer: Du animierst ausschließlich innerhalb von
@media (prefers-reduced-motion: no-preference). Dann ist die ruhige Variante der Standard.
@media (prefers-reduced-motion: no-preference) {
.dialog {
transition: transform 200ms ease-out;
}
}
Reduzierte Bewegung heißt nicht, dass jede Rückmeldung verschwindet. Ersetze die Bewegung durch eine Überblendung der Deckkraft oder durch einen sofortigen Wechsel. Testen kannst du das über die Systemeinstellung oder in den DevTools unter „Rendering“ mit der Emulation von prefers-reduced-motion. Der komplette Durchgang steht unter Barrierefreiheit selbst testen.
Was die WCAG verlangt
Drei Erfolgskriterien betreffen Bewegung unmittelbar.
2.2.2 Pausieren, Beenden, Ausblenden auf Stufe A greift, wenn drei Bedingungen zusammenkommen: Die Bewegung startet automatisch, sie läuft länger als fünf Sekunden, und sie steht parallel zu anderem Inhalt. Dann braucht es einen Mechanismus zum Pausieren, Beenden oder Ausblenden. Ein Loop mit infinite erfüllt die ersten beiden Bedingungen immer. Der Schalter setzt in der Praxis animation-play-state: paused über eine Klasse. Eine vollständige Komponente zeigt Slider und Karussells.
2.3.1 Grenzwert von drei Blitzen auf Stufe A verbietet mehr als drei Blitze pro Sekunde. Eine Blink-Animation, die opacity zwischen 0 und 1 mit einer Dauer von 200 Millisekunden wechselt, erzeugt fünf Wechsel pro Sekunde und verstößt gegen das Kriterium. Bei größeren Flächen mit starkem Helligkeitsunterschied kann das epileptische Anfälle auslösen.
2.3.3 Animation aus Interaktionen steht auf Stufe AAA und verlangt, dass Bewegung aus einer Bedienhandlung abschaltbar ist. Der Reduce-Motion-Block oben erfüllt das bereits. Die weiteren Anforderungen dieser Stufe stehen unter WCAG-AAA-Kriterien.
Fokus, Kontrast und Dauer
Der Fokusring darf nie weganimiert werden. Eine Animation, die outline entfernt oder ein fokussiertes Element aus dem Sichtfeld schiebt, verstößt gegen 2.4.7 Fokus sichtbar. Gestalte den Fokus über outline-offset oder box-shadow und lass den Ring stehen.
Animierte Inhalte brauchen in jedem Zustand ausreichenden Kontrast. Eine Einblendung über zwei Sekunden zeigt den Text die längste Zeit bei einer Deckkraft unter 1, und in dieser Phase liegt das Kontrastverhältnis unter dem geforderten Wert. Kurze Dauern lösen das Problem.
Parallax-Effekte sind eine der häufigsten Ursachen für Beschwerden. Hintergrund und Vordergrund bewegen sich unterschiedlich schnell, und diese Diskrepanz reizt das vestibuläre System. Wenn ein Parallax-Effekt bleiben soll, schalte ihn bei reduzierter Bewegung vollständig ab.
Eine Animationsdauer über 500 Millisekunden wirkt bei kognitiven Einschränkungen störend, weil sie die Aufmerksamkeit bindet und die nächste Handlung verzögert. Halte Rückmeldungen unter 200 Millisekunden und Übergänge größerer Flächen unter 400 Millisekunden. Mehr dazu steht unter kognitive Barrierefreiheit, unter Kognition und Neurodiversität und für Lagen wie das Lesen im fahrenden Zug unter situative und temporäre Einschränkungen.
Häufige Fragen
Wie lasse ich eine CSS-Animation endlos laufen?
Setze animation-iteration-count: infinite oder schreibe den Wert in die Kurzform, etwa animation: drehen 900ms linear infinite. Der Ablauf läuft dann, bis das Element verschwindet oder die Eigenschaft entfällt. Sobald ein solcher Loop automatisch startet und länger als fünf Sekunden läuft, braucht er nach WCAG 2.2.2 einen Schalter zum Anhalten.
Wie erzeuge ich eine Pause zwischen zwei Durchläufen?
animation-delay wartet nur einmal vor dem ersten Durchlauf. Für eine Pause zwischen den Wiederholungen verlängerst du die Gesamtdauer und hältst den Ruhezustand über Prozentschritte. Läuft die Bewegung bis 30 Prozent und steht der Wert bei 100 Prozent wieder auf dem Ausgangspunkt, entsteht eine Ruhephase von 70 Prozent der Dauer.
Wann nehme ich transition und wann animation?
Nimm transition, wenn eine Handlung den Zustand ändert, etwa ein Hover oder eine Klasse aus JavaScript. Es gibt dann zwei Werte, und der Browser füllt die Zeit dazwischen. Nimm animation, wenn der Ablauf von selbst startet, sich wiederholt oder mehr als zwei Stationen hat. Ein Spinner gehört zur zweiten Gruppe.
Warum ruckelt meine CSS-Animation?
Meist animiert sie eine Eigenschaft, die das Layout ändert, etwa width, height, margin oder top. Der Browser rechnet dann bei jedem Bild die Geometrie neu und verpasst das Zeitbudget. Stelle die Animation auf transform und opacity um. Im Performance-Panel der DevTools siehst du die Layout-Phasen als lila Blöcke.
Muss ich prefers-reduced-motion auch bei kurzen Übergängen umsetzen?
Die Umsetzung ist Pflicht. Die Einstellung meldet ein gesundheitliches Bedürfnis, und der Aufwand liegt bei wenigen Zeilen. Ein globaler Medienblock am Ende des Stylesheets deckt auch alles ab, was später dazukommt. Kurze Überblendungen der Deckkraft dürfen bleiben, weil sie keine Positionsänderung zeigen. Bewegungen im Raum werden abgeschaltet.
Verwandte Themen
- overflow und Scrollen: warum
scroll-behavior: smoothin den Reduce-Motion-Block gehört - Slider und Karussells: Pause-Schalter und Live-Region an einer vollständigen Komponente
- WCAG 2.2.2 Pausieren, Beenden, Ausblenden: der Normtext mit den drei Bedingungen
- Akkordeons: Aufklappen mit Übergang und intakter Tastaturbedienung
- Core Web Vitals: wie sich Ruckeln in INP und CLS niederschlägt
Quellen
- Using CSS animations (MDN: @keyframes und die animation-Eigenschaften)
- prefers-reduced-motion (MDN: die Werte reduce und no-preference)
- Understanding SC 2.2.2 Pause, Stop, Hide (W3C: die drei Bedingungen des Kriteriums)
- Understanding SC 2.3.3 Animation from Interactions (W3C: Stufe AAA, Bewegung aus Bedienhandlungen)