mitmario.dev

Bewegung mit Rücksicht

CSS Im Browser 4 Min Lesezeit 4 BeispieleLektion 4 von 6

Bewegung kann schaden, und zwar aus zwei ganz verschiedenen Gründen. Sie kann Menschen krank machen. Und sie kann Geräte ausbremsen.

Die Systemeinstellung fragen

Was reduce ändern soll
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  background-color: #f4f4f5;
}

@keyframes drehen {
  from {
    rotate: 0deg;
  }

  to {
    rotate: 360deg;
  }
}

.projekt {
  width: 260px;
  padding: 24px;
  border-radius: 12px;
  background-color: #ffffff;
  transition: transform 0.2s ease-out;
}

.projekt:hover {
  transform: translateY(-6px);
}

.projekt h3 {
  margin: 0 0 4px 0;
  font-size: 18px;
}

.projekt p {
  margin: 0;
  color: #52525b;
}

.ladezone {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 24px;
  color: #52525b;
}

.spinner {
  width: 32px;
  height: 32px;
  border: 4px solid #dbeafe;
  border-top-color: #2563eb;
  border-radius: 50%;
  animation: drehen 0.8s linear infinite;
}

/* Nicht abschalten, sondern ersetzen. */
@media (prefers-reduced-motion: reduce) {
  .projekt:hover {
    transform: none;
    background-color: #eff6ff;
  }

  .spinner {
    animation-duration: 2.4s;
  }
}

.hinweis {
  max-width: 60ch;
  margin-top: 32px;
  color: #52525b;
}

Alle großen Betriebssysteme haben einen Schalter für reduzierte Bewegung. Wer ihn einschaltet, tut das meistens nicht aus Geschmack, sondern weil ihm von Animationen schwindelig oder übel wird. Im CSS liest du den Schalter mit @media (prefers-reduced-motion: reduce).

Naheliegend ist, darin einfach alles abzuschalten. Das klingt rücksichtsvoll und macht die Oberfläche schlechter, denn ohne jede Rückmeldung weiß niemand mehr, ob ein Klick angekommen ist oder ob im Hintergrund noch etwas lädt.

So nicht, alles abgeschaltet
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  background-color: #f4f4f5;
}

@keyframes drehen {
  from {
    rotate: 0deg;
  }

  to {
    rotate: 360deg;
  }
}

.titel {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #52525b;
}

.fassung {
  margin-bottom: 32px;
}

.projekt {
  padding: 16px 24px;
  border-radius: 12px;
  background-color: #ffffff;
}

.projekt h3 {
  margin: 0 0 4px 0;
  font-size: 18px;
}

.projekt p {
  margin: 0;
  color: #52525b;
}

.ladezone {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  color: #52525b;
}

.spinner {
  width: 32px;
  height: 32px;
  border: 4px solid #dbeafe;
  border-top-color: #2563eb;
  border-radius: 50%;
  animation: drehen 0.8s linear infinite;
}

/* Beide Fassungen zeigen, was bei reduzierter Bewegung
   übrig bleibt. Im echten Projekt steht jede davon in
   @media (prefers-reduced-motion: reduce). */

/* So nicht: Der Spinner steht still und die Karte
   antwortet auf nichts. */
.aus .spinner {
  animation: none;
}

/* So: Die Karte färbt sich, der Spinner wird langsamer. */
.ersetzt .projekt:hover {
  background-color: #eff6ff;
}

.ersetzt .spinner {
  animation-duration: 2.4s;
}

Im Beispiel stehen beide Fassungen untereinander, so wie sie bei eingeschalteter Einstellung aussähen. In der oberen ist alles abgeschaltet. Der Spinner steht still und sieht aus wie ein fertiger Ring, und die Karte reagiert gar nicht mehr auf den Zeiger. In der unteren dreht sich der Spinner weiter, nur dreimal langsamer, und die Karte färbt sich hellblau, statt sich anzuheben.

Die Barrierefreiheitsrichtlinien WCAG 2.2 verlangen in Kriterium 2.3.3 (Stufe AAA) nur, dass sich Bewegung abschalten lässt, die eine Bedienung auslöst. Ich gehe einen Schritt weiter, meine Regel heißt ersetzen statt abschalten. Statt einer Bewegung ein Farbwechsel, ein Rahmen, ein Wechsel der Deckkraft. Statt einer Einblendung über eine halbe Sekunde dieselbe Einblendung in einer Hundertstel. Und ein Ladeindikator bleibt ein Ladeindikator. Er wird höchstens langsamer.

Was in dem Block landet, ist damit meistens kurz, nämlich die zwei oder drei Stellen, an denen sich wirklich etwas über den Bildschirm bewegt.

Was flüssig läuft und was nicht

Teuer und billig nebeneinander
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

@keyframes wandern-teuer {
  from {
    width: 40px;
    margin-left: 0px;
  }

  to {
    width: 80px;
    margin-left: 220px;
  }
}

@keyframes wandern-billig {
  from {
    scale: 1;
    translate: 0px 0px;
  }

  to {
    scale: 2 1;
    translate: 220px 0px;
  }
}

.etikett {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #52525b;
}

.bahn {
  margin-bottom: 32px;
  padding: 8px;
  border-radius: 8px;
  background-color: #f4f4f5;
}

.kasten {
  display: block;
  width: 40px;
  height: 24px;
  border-radius: 6px;
  background-color: #2563eb;
}

.teuer {
  animation: wandern-teuer 2s ease-in-out infinite alternate;
}

.billig {
  transform-origin: left center;
  animation: wandern-billig 2s ease-in-out infinite alternate;
}

.hinweis {
  max-width: 60ch;
  color: #52525b;
}

Für jedes Bild einer Animation macht der Browser bis zu drei Dinge. Er rechnet aus, wie groß und wo alles ist. Er zeichnet die Flächen. Und er setzt die fertigen Ebenen zusammen. Der dritte Schritt ist billig, die ersten beiden sind es nicht.

Animierst du width, height, margin, padding, top oder left, muss er alle drei Schritte machen. Oft betrifft das große Teile der Seite, weil sich Größen an die Nachbarn weiterreichen. Bei transform und opacity reicht der dritte. Ich animiere deshalb nur transform und opacity.

Die getrennten Eigenschaften translate, rotate und scale aus Lektion 13.2 zählen dabei mit, sie sind derselbe Mechanismus in anderer Schreibweise. Alles andere ist nicht verboten. Es ist dann aber eine bewusste Entscheidung, und für eine Bewegung, die mehrere Sekunden läuft, selten die richtige.

Im Beispiel kannst du dem Unterschied beim Laufen zusehen. Am festgeklickten oberen Kasten ändert sich width dauernd (rund 79 dann 77 dann 72) und margin gleich mit (0px 0px 0px 216px, dann 205, dann 174). Das sind zwei Layoutwerte, und zwar in jedem einzelnen Bild. Am unteren steht width: 40px still und margin: 0px ebenso, dafür laufen translate (rund 218 dann 202 dann 169) und scale (1.99 1 dann 1.92 1). Auf dem Bildschirm ist es dieselbe Bewegung, aber dahinter steckt ein völlig anderer Aufwand.

will-change ist keine Gewohnheit

Mit will-change: transform weiß der Browser vorab, dass sich hier etwas ändern wird, und kann das Element schon mal auf eine eigene Ebene legen. Das hilft gegen ein Ruckeln im ersten Moment einer Animation.

Es kostet aber Speicher, und zwar dauerhaft, solange die Angabe im Stylesheet steht. Auf hundert Karten gesetzt macht es die Seite langsamer statt schneller. will-change gehört deshalb nicht fest in eine Komponente. Es ist die letzte Maßnahme gegen einen Engpass, den du konkret gemessen hast.

Endlos ist fast immer zu viel

Endlos im Blickfeld
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

@keyframes pochen {
  0% {
    scale: 1;
  }

  50% {
    scale: 1.18;
  }

  100% {
    scale: 1;
  }
}

.angebot {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  border-radius: 12px;
  background-color: #fef9c3;
}

.stoerer {
  flex-shrink: 0;
  padding: 8px 16px;
  border-radius: 999px;
  font-size: 14px;
  color: #ffffff;
  background-color: #b91c1c;
  animation: pochen 0.7s ease-in-out infinite;
}

.angebot p {
  margin: 0;
}

.hinweis {
  max-width: 60ch;
  margin-top: 24px;
  color: #52525b;
}

Eine Animation mit infinite hört nie auf, und das menschliche Auge ist darauf gebaut, Bewegung am Rand des Blickfelds zu bemerken. Ein pochender Störer neben einem Absatz macht den Absatz unlesbar, auch für Leute ohne jede Empfindlichkeit.

Für Bewegung, die von selbst startet, länger als fünf Sekunden läuft und neben anderem Inhalt steht, verlangt WCAG 2.2 schon auf Stufe A, dass man sie anhalten kann (Kriterium 2.2.2). Der Störer im Beispiel lässt sich nicht anhalten.

Es gibt gute Fälle. Ein Ladeindikator, solange wirklich geladen wird, oder eine kleine Bewegung, die nach zwei Durchläufen aufhört. Es kommt darauf an, ob die Bewegung etwas mitteilt oder nur Aufmerksamkeit abgreift.

Die Systemeinstellung beim Prüfen

Beim Prüfen lässt sich prefers-reduced-motion nicht umschalten, es gilt immer die Einstellung deines Systems. Deshalb steht der Media-Query-Block zwar in der Aufgabe, aber nie in der Prüfliste. Abgefragt wird er im Abschnittstest.

Deine eigene Einstellung kommt dagegen an. In der Eingabezeile der Console antwortet matchMedia('(prefers-reduced-motion: reduce)').matches mit false oder true, je nachdem, wie das System eingestellt ist. Legst du den Schalter im Betriebssystem um, dreht sich die Antwort, und neu laden musst du dafür nichts.

Was der Block bewirkt, steht im Auskunftsstreifen. Bei der Karte im ersten Beispiel steht beim Überfahren mit ausgeschaltetem Schalter transform: matrix(1, 0, 0, 1, 0, -6). Die Karte ist also sechs Pixel angehoben, und der Grund ist weiß. Mit eingeschaltetem Schalter fehlt die transform-Zeile ganz, dafür steht dort background-color: rgb(239, 246, 255). Am Ring wechselt die animation-Zeile von drehen 0.8s linear infinite auf drehen 2.4s linear infinite. Ersetzt statt abgeschaltet, in drei Zeilen nachzulesen.

In der Aufgabe stehen eine Meldung, zwei Projektkarten und ein Spinner, und jedes der drei bewegt sich teurer oder rücksichtsloser als nötig.

Zum Mitnehmen

Unter prefers-reduced-motion: reduce wird Bewegung ersetzt, nicht abgeschaltet. Bewegt werden nur transform und opacity, will-change nur gegen einen gemessenen Engpass, und Endlosbewegung nur, wenn sie etwas mitteilt.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.