CSS bietet dir zwei Werkzeuge für Bewegung: Transitions und @keyframes-Animationen. Die meisten Entwickler wissen, dass beide existieren, greifen aber zu dem, was sie zuerst gelernt haben — meistens Transitions — und fragen sich dann, warum der Ladespinner nicht loopt oder warum die Benachrichtigung in ihre ursprüngliche Position zurückschnappt. Die Entscheidung ist nicht kompliziert, wenn du verstehst, wofür jede gemacht ist. Transitions sind eine Reaktion: Etwas ändert den Zustand, der Browser glättet die Änderung. @keyframes-Animationen sind eigenständige Skripte: Sie laufen für sich allein, loopen wenn gewünscht und brauchen keinen Zustandstrigger. Dieser Leitfaden behandelt beides, die wichtigsten Eigenschaften und die Performance-Details, die geschmeidige 60fps-Animationen von ruckelnden unterscheiden. Wenn du viel CSS schreibst, lohnt es sich, den CSS Formatter zu bookmarken — er räumt unordentliche Shorthands schnell auf.
CSS Transitions — Der einfache Fall
Eine Transition sagt dem Browser: "Wenn sich diese Eigenschaft ändert, animiere die Änderung über Zeit statt zu springen." Das ist alles. Der häufigste Anwendungsfall ist Hover-Effekte — Farbänderungen, Opacity-Fades, subtile Scale-Transforms. Der MDN-Transitions-Leitfaden deckt den vollen Spec ab, aber die vier Eigenschaften, die wirklich wichtig sind, sind transition-property, transition-duration, transition-timing-function und transition-delay.
/* Der Shorthand: property duration easing delay */
.btn-primary {
background-color: #4f46e5;
color: #fff;
padding: 0.625rem 1.25rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 200ms ease, transform 150ms ease;
}
.btn-primary:hover {
background-color: #4338ca;
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
}
/* Opacity-Fade — eine Karte, die ins Bild kommt */
.notification-card {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.notification-card.is-visible {
opacity: 1;
}Du kannst mehrere Transitions durch Kommas getrennt auflisten, jede mit eigener Dauer und eigenem Easing — das ist es, was das Button-Beispiel oben tut. Jede Eigenschaft animiert unabhängig.
transition: all. Es ist verführerisch als Catch-all, bedeutet aber, dass jede Eigenschaftsänderung an diesem Element animiert wird — auch solche, die du nicht beabsichtigt hast. Außerdem lässt es den Browser bei jedem Frame mehr Arbeit machen. Liste die spezifischen Eigenschaften auf, die du transitionieren willst. Die MDN-transition-property-Docs erklären, welche Eigenschaften animierbar sind.transition-timing-function — Easing erklärt
Die Timing-Funktion steuert die Beschleunigungskurve der Animation — ob sie schnell startet und verlangsamt, sich gleichmäßig bewegt oder durch eine benutzerdefinierte Kurve springt. Die eingebauten Keywords decken die meisten Situationen ab.
ease— startet langsam, beschleunigt, verlangsamt sich dann gegen Ende. Der Standard. Gut für die meisten UI-Bewegungen.linear— konstante Geschwindigkeit. Gut für Spinner und Fortschrittsbalken, schlecht für die meisten anderen Dinge — wirkt mechanisch.ease-in— startet langsam, endet schnell. Fühlt sich an wie etwas, das Fahrt aufnimmt. Gut für Elemente, die den Bildschirm verlassen.ease-out— startet schnell, endet langsam. Fühlt sich natürlich an für Elemente, die in den Bildschirm eintreten (wie eine eingleitende Benachrichtigung).ease-in-out— langsam an beiden Enden. Wirkt poliert für Elemente, die im Bild starten und anhalten.cubic-bezier(x1, y1, x2, y2)— definiere deine eigene Kurve. Tools wie cubic-bezier.com lassen dich benutzerdefinierte Kurven visuell erstellen und vorschauen.
/* Benutzerdefiniertes federartiges Easing mit cubic-bezier */
.drawer {
transform: translateX(-100%);
transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.drawer.is-open {
transform: translateX(0);
}
/* Die cubic-bezier-Werte überschießen leicht (y > 1)
was beim Öffnen einen befriedigenden Federeffekt ergibt */@keyframes — Wenn Transitions nicht ausreichen
Transitions haben ein hartes Limit: Sie brauchen zwei Zustände und einen Trigger. Wenn du etwas unbegrenzt loopen, beim Laden der Seite ohne Interaktion ausführen oder durch mehr als zwei Schritte animieren willst, brauchst du @keyframes. Die MDN @keyframes-Referenz deckt die vollständige Syntax ab. Du definierst die Keyframes separat und verbindest sie dann mit der animation-Eigenschaft mit einem Element.
/* Die Animation definieren */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* An ein Element anhängen */
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
/* Mehrstufig — mehr als zwei Haltepunkte */
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge-live {
animation: pulse-ring 1.5s ease-in-out infinite;
}Der animation-Shorthand — Alle acht Eigenschaften
Der animation-Shorthand packt acht Eigenschaften in eine Deklaration. Du brauchst nicht jedes Mal alle, aber zu wissen, was jede steuert, erspart dir das Durchsuchen von Dokumentation, wenn etwas nicht wie erwartet funktioniert.
/*
animation: name | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state
*/
.toast {
animation: slide-in-right 350ms ease-out 0s 1 normal forwards running;
}
/* Häufiger nur mit den benötigten Werten geschrieben: */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/* Zur Klarheit in einzelne Eigenschaften aufgeteilt: */
.confetti-piece {
animation-name: float-down;
animation-duration: 2s;
animation-timing-function: ease-in;
animation-delay: calc(var(--i) * 150ms); /* gestaffelt via CSS custom property */
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Eine Animation via JavaScript pausieren: Klasse umschalten */
.spinner.is-paused {
animation-play-state: paused;
}animation-fill-mode — Das Snap-Back-Problem beheben
Das ist die Eigenschaft, über die jeder irgendwann stolpert. Standardmäßig springt das Element zurück zu seinen Vor-Animations-Stilen, wenn eine Animation endet — als wäre die Animation nie passiert. Wenn du eine Toast-Benachrichtigung von rechts eingleitest, springt sie in dem Moment, wo die Animation endet, wieder aus dem Bildschirm. animation-fill-mode: forwards behebt das, indem das Element nach Ende der Animation in seinem finalen Keyframe-Zustand gehalten wird.
@keyframes slide-in-right {
from {
transform: translateX(110%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Ohne forwards: Toast gleitet rein, SPRINGT dann zurück aus dem Bildschirm */
.toast-bad {
animation: slide-in-right 350ms ease-out;
}
/* Mit forwards: Toast gleitet rein und BLEIBT an Position */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/*
fill-mode-Werte:
- none (Standard) — keine Stile vor oder nach
- forwards — finalen Keyframe nach Animationsende halten
- backwards — ersten Keyframe während der Verzögerungsphase anwenden
- both — forwards + backwards kombiniert
*/Echte Beispiele — Skeleton Loader, Spinner, Toast, pulsierendes Badge
Hier sind vier Muster, die du ständig verwenden wirst. Jedes demonstriert eine spezifische Kombination von @keyframes und Animations-Eigenschaften.
/* --- 1. Skeleton-Lade-Shimmer --- */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
border-radius: 4px;
}
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text { height: 14px; width: 100%; margin-bottom: 8px; }
.skeleton-text:last-child { width: 80%; }/* --- 2. Drehender Loader --- */
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader-spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid rgba(79, 70, 229, 0.2);
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 600ms linear infinite;
}/* --- 3. Eingleitender Benachrichtigungs-Toast --- */
@keyframes toast-enter {
from {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toast-exit {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
}
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.875rem 1.25rem;
background: #1f2937;
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
animation: toast-enter 350ms ease-out forwards;
}
.toast.is-dismissing {
animation: toast-exit 300ms ease-in forwards;
}/* --- 4. Pulsierendes Benachrichtigungs-Badge --- */
@keyframes badge-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
}
.notification-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 999px;
animation: badge-pulse 2s ease-in-out infinite;
}Performance — GPU-kompositierte vs Layout-auslösende Eigenschaften
Nicht alle CSS-Eigenschaften sind gleich, wenn es um Animations-Performance geht. Die Browser-Rendering-Pipeline hat drei Stufen, die hier wichtig sind: Layout (Berechnen von Elementgrößen und -positionen), Paint (Pixel füllen) und Composite (Ebenen auf dem GPU zusammenführen). Eine Eigenschaft zu animieren, die Layout auslöst, bedeutet, dass der Browser bei jedem Frame die gesamte Dokumentgeometrie neu berechnet — das ist teuer und verursacht Ruckeln. Der Animationsperformance-Leitfaden von web.dev geht da in die Tiefe.
- Sicher zu animieren (GPU-kompositiert):
transform(translate, scale, rotate) undopacity. Diese laufen vollständig auf dem GPU-Compositor-Thread — sie lösen nie Layout oder Paint aus. - Verursacht Paint (wenn möglich vermeiden):
color,background-color,border-color,box-shadow. Diese überspringen Layout, lösen aber bei jedem Frame ein Repaint aus. Kurze Transitions (unter 300ms) sind meistens in Ordnung. - Verursacht Layout (niemals animieren):
width,height,top,left,margin,padding. Jeder Frame löst eine vollständige Layout-Neuberechnung aus — garantiertes Ruckeln auf komplexen Seiten.
transform und opacity animieren. Wenn du etwas bewegen willst, verwende translate statt left/top. Wenn du etwas skalieren willst, verwende scale statt width/height. CSS Triggers ist eine Referenz, die genau auflistet, welche Rendering-Stufen jede Eigenschaft betrifft./* Schlecht — width animieren löst bei jedem Frame Layout aus */
.progress-bar-bad {
transition: width 300ms ease;
}
/* Gut — stattdessen scaleX transform verwenden */
.progress-bar {
transform-origin: left center;
transition: transform 300ms ease;
}
/* In JS setze: progressBar.style.transform = 'scaleX(0.75)' für 75% */
/* Schlecht — top/left animieren (positioniert Element mit Layout) */
.tooltip-bad {
position: absolute;
top: 0;
transition: top 200ms ease;
}
/* Gut — stattdessen translateY verwenden */
.tooltip {
position: absolute;
top: 0;
transform: translateY(0);
transition: transform 200ms ease;
}will-change — Sparsam verwenden
will-change ist ein Hinweis an den Browser, dass eine bestimmte Eigenschaft gleich animiert wird, sodass er das Element im Voraus auf seine eigene Compositor-Ebene hochstufen soll. Das kann den kurzen Ruckel-Flash eliminieren, den man manchmal ganz am Anfang einer Animation auf Low-End-Hardware sieht. Aber es hat einen echten Preis: Jede hochgestufte Ebene verbraucht GPU-Speicher. Wenn du will-change: transform auf jedes animierte Element in deiner App setzt, verschlechterst du die Performance auf den meisten Geräten — das Gegenteil von dem, was du wolltest.
/* Richtig — kurz vor Animationsstart hinzufügen, danach entfernen */
.modal-overlay {
/* Hier kein will-change standardmäßig setzen */
}
/* Nur via JavaScript hinzufügen, wenn der Benutzer das Modal öffnet */
/* overlay.style.willChange = 'opacity'; */
/* overlay.addEventListener('transitionend', () => { */
/* overlay.style.willChange = 'auto'; */
/* }); */
/* In CSS für Elemente akzeptabel, die häufig animieren
(z.B. ein dauerhafter Floating-Action-Button beim Scrollen) */
.fab {
will-change: transform; /* OK — dieses Element animiert wirklich beim Scrollen */
transition: transform 200ms ease;
}
/* Das nicht tun — verschwendet GPU-Speicher ohne Nutzen */
.card {
will-change: transform; /* SCHLECHT — Karte animiert nur beim Hover, nicht dauerhaft */
}prefers-reduced-motion — Barrierefreiheit, die du nicht überspringen kannst
Ein erheblicher Teil der Benutzer hat vestibuläre Störungen oder andere Zustände, bei denen Bewegung Unbehagen oder Übelkeit auslöst. Die prefers-reduced-motion-Media-Query lässt dich die "Bewegung reduzieren"-Einstellung auf Systemebene respektieren. Die WCAG 2.1-Richtlinie 2.3.3 behandelt diese Anforderung, und die MDN-prefers-reduced-motion-Referenz zeigt die Browser-Unterstützung (sie ist jetzt universell). Das Muster ist einfach: Wickle deine Animationen in die Standard-Media-Query und stelle einen Kein-Bewegung-Fallback innerhalb der Reduced-Motion-Query bereit.
/* Animationen normal definieren für Benutzer, die mit Bewegung einverstanden sind */
@keyframes slide-in-right {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
.spinner {
animation: spin 600ms linear infinite;
}
/* Override für Benutzer, die reduzierte Bewegung bevorzugen */
@media (prefers-reduced-motion: reduce) {
.toast {
/* Kein Gleiten — einfach erscheinen */
animation: none;
opacity: 1;
transform: none;
}
.spinner {
/* Stark verlangsamen oder ganz stoppen */
animation-duration: 4s;
}
/* Nuklear-Option — ALLE Animationen seitenübergreifend deaktivieren */
/* Nur verwenden, wenn du jede Animation nicht einzeln geprüft hast */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Die "Nuklear-Option" am Ende dieses Snippets ist ein häufiges Muster für bestehende Codebases, die nicht jede Animation geprüft haben. Es ist besser als nichts, aber jede Animation einzeln zu prüfen gibt dir mehr Kontrolle — einige Animationen vermitteln Zustand (ein Fortschrittsbalken, ein Lade-Spinner) und sollten beibehalten, nur verlangsamt werden.
Transitions vs @keyframes — Der Entscheidungsleitfaden
Wenn du dir nicht sicher bist, welches du verwenden sollst, stelle zwei Fragen zur Animation, die du baust:
- Wird sie durch eine Zustandsänderung ausgelöst? (Hover, Focus, Klassen-Toggle, Checkbox) → Verwende eine Transition. Genau dafür sind Transitions gemacht.
- Loopt sie unbegrenzt? → Verwende @keyframes mit
animation-iteration-count: infinite. - Hat sie mehr als zwei Schritte? (nicht nur Start → Ende, sondern Start → Mitte → Ende oder mehr) → Verwende @keyframes.
- Muss sie beim Laden der Seite, ohne Benutzerinteraktion, laufen? → Verwende @keyframes.
- Muss das Element nach dem Ende in seinem Endzustand bleiben? → Verwende @keyframes mit
animation-fill-mode: forwards.
Zusammenfassung
Transitions behandeln zustandsgetriebene Bewegung. @keyframes behandelt alles andere. Für Performance immer transform und opacity animieren — width, height, top und left außen vor lassen. animation-fill-mode: forwards verwenden, wenn das Element seinen Endzustand halten soll. Den prefers-reduced-motion-Override vor dem Veröffentlichen hinzufügen — es ist eine zehnzeilige Ergänzung, die echten Benutzern viel bedeutet. Für mehr über das Compositing-Modell und was Layout auslöst, sind die web.dev-Rendering-Performance-Docs die praktischste verfügbare Ressource. Sobald du das CSS geschrieben hast, lass es durch den CSS Formatter laufen, um den Shorthand lesbar zu halten, oder durch den CSS Minifier, um Whitespace vor der Veröffentlichung zu entfernen.