# Prefers Reduced Motion React ohne Hydration Mismatch

> Prefers reduced motion React Guide: Respektier die OS-Einstellung in Next.js ohne Hydration Mismatch und ersetz Springs durch kurze Fades, die Feedback geben.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 13. Okt. 2026
- Kategorie: [Next.js und Tailwind CSS](https://wingo-ui.com/de/blog/category/nextjs-tailwind)
- Lesezeit: 8 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/prefers-reduced-motion-react
- Englische Fassung: https://wingo-ui.com/blog/prefers-reduced-motion-react

## Kurz gesagt

Um prefers-reduced-motion in einer React-App mit Server-Rendering zu respektieren, entscheidest du so viel wie möglich in CSS (die Varianten motion-safe: und motion-reduce: von Tailwind), setzt einmal MotionConfig reducedMotion="user" und liest die Einstellung in Komponenten über einen useSyncExternalStore-Hook, dessen Server-Snapshot false ist. Denn useReducedMotion von Motion liefert auf dem Server null und im Browser einen anderen Wert. Ist die Einstellung aktiv, ersetzt du Bewegung durch einen kurzen Opacity-Fade, statt Animationen abzuschalten, damit jede Zustandsänderung sichtbar bleibt.

Die meisten Antworten zu prefers reduced motion React sind ein einzeiliger Hook und ein CSS-Snippet, das jede Animation auf `0.01ms` setzt. Dann meldet die Next.js-Konsole einen Hydration Mismatch, sobald jemand mit aktivierter Option „Bewegung reduzieren“ die App öffnet, und der Dialog, der früher hereinskaliert ist, poppt jetzt ganz ohne Feedback auf. Dieses Tutorial zeigt, worum die Einstellung bittet, wo du sie entscheidest (CSS, eine Motion-Config oder ein Hook), warum Motions eigener Hook die Hydration stört und was du statt einer Spring einsetzt, damit die Oberfläche lesbar bleibt.

Die Fakten unten haben wir am 13. Oktober 2026 im Quellcode von Motion 12.43, React 19.3 und Tailwind CSS 4.3.3 geprüft.

## Worum bittet prefers-reduced-motion?

Um weniger Bewegung. Das Media Feature greift, wenn jemand unter macOS oder iOS „Bewegung reduzieren“ aktiviert, unter Windows 11 die Animationseffekte ausschaltet, unter Android „Animationen entfernen“ wählt oder das Gegenstück unter GNOME und KDE nutzt; [MDN listet jeden Weg auf](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). Die Gründe dafür sind verschieden: Gleichgewichtsstörungen, Migräne, Ablenkung oder ein Handy, das sich ohne Übergänge schneller anfühlt.

Das [WCAG-Understanding-Dokument zu 2.3.3](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html) benennt das Risiko: Nicht wesentliche Bewegung kann Schwindel, Übelkeit und Kopfschmerzen auslösen, und als Beispiele nennt es Parallax und Bewegung beim Scrollen. Dieses Kriterium hat Stufe AAA. Das Kriterium auf Stufe A, das für jede Website gilt, ist [2.2.2 Pause, Stop, Hide](https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html), für Bewegung, die von selbst startet und länger als fünf Sekunden läuft: Karussells, Laufschriften, Hintergründe in Endlosschleife.

Die Einstellung verlangt keine eingefrorene Oberfläche. Ein brauchbarer Test für barrierefreie Animation: Behalte die Information, die eine Animation trägt (etwas hat sich geöffnet, etwas wurde gespeichert, dieses Element ist dorthin gewandert), und lass die Bewegung von A nach B weg.

## Wie gehst du in Tailwind mit prefers-reduced-motion um?

In Tailwind nutzt du die Varianten `motion-safe:` und `motion-reduce:` und fängst am besten genau dort an, denn CSS hat kein Hydration-Problem: Die Media Query greift beim ersten Paint, bevor irgendein JavaScript läuft. So kompiliert Tailwind 4.3.3 sie:

```css
@media (prefers-reduced-motion: no-preference) {
  @media (hover: hover) {
    .motion-safe\:hover\:-translate-y-0\.5:hover {
      --tw-translate-y: calc(var(--spacing) * -0.5);
      translate: var(--tw-translate-x) var(--tw-translate-y);
    }
  }
}
@media (prefers-reduced-motion: reduce) {
  .motion-reduce\:transition-none {
    transition-property: none;
  }
}
```

`motion-safe:` fügt Bewegung nur für Nutzer hinzu, die nicht um weniger gebeten haben. Die statische Version ist also die Basis, und Bewegung ist das Extra. `motion-reduce:` entfernt Bewegung, die du ohne Bedingung hinzugefügt hast. Unsere [Card](https://wingo-ui.com/components/card) hebt sich beim Hover mit `motion-safe:hover:-translate-y-0.5` an und behält den Schattenwechsel für alle. Der Hover bleibt bei reduzierter Bewegung also erkennbar, nur ohne die Bewegung.

Mehr gehört zum Thema prefers-reduced-motion Tailwind bei CSS-Transitions, Keyframe-Animationen und Spinnern nicht. Nutz die Varianten für alles, was sich ohne JavaScript entscheiden lässt.

## Warum verursacht useReducedMotion einen Hydration Mismatch?

Weil der Hook auf dem Server einen anderen Wert liefert. In Motion 12.43 startet `prefersReducedMotion.current` in motion-dom als `null` (laut Kommentar im Code ist das der Wert auf dem Server), und `useReducedMotion()` kopiert ihn beim ersten Render in `useState`. Auf dem Server ist das `null`. Im Browser liest der Hook die Media Query direkt vor `useState`, also bekommt die Hydration `true` oder `false`. Der Hook hält den Wert im State und aktualisiert ihn nie; ein TODO im Quellcode fragt, ob Leute Live-Updates vermissen.

Wir haben jeden Ansatz mit `renderToString` von React gerendert, ihn in Chromium über Playwright mit und ohne `reducedMotion: "reduce"` hydriert und die Konsole gelesen:

| Ansatz | Reduzierte Bewegung an | Reduzierte Bewegung aus | Folgt einer Änderung der Einstellung |
| --- | --- | --- | --- |
| Motion `useReducedMotion()` wählt `initial` | `A tree hydrated but some attributes ... didn't match` | Sauber als Boolean; in ein Attribut gerendert ist es `null` gegen `false`, ein Mismatch bei jedem Besuch | Nein, einmal in den State gelesen |
| `useSyncExternalStore`-Hook, Server-Snapshot `false` | Sauber; eine Eingangsanimation, die während der Hydration läuft, spielt einmal vollständig ab | Sauber | Ja |
| `<MotionConfig reducedMotion="user">` | Sauber; Transforms springen ans Ende, Opacity wird weiter animiert | Sauber | Für Elemente, die nach der Änderung mounten |

Die erste Tabellenzeile fasst das Problem useReducedMotion Hydration zusammen. Das übliche Muster sieht harmlos aus:

```tsx
// falsch in einer server-gerenderten App: null auf dem Server, true im Browser
const reduce = useReducedMotion();
return <motion.div initial={reduce ? false : { opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} />;
```

Ist die Einstellung an, rendert der Server `opacity: 0; transform: translateY(8px)` ins style-Attribut, der Browser erwartet den Endzustand, und React meldet im Entwicklungsmodus den Mismatch. React korrigiert das Attribut nicht; Motion schreibt die Styles direkt nach dem Mount neu. Besucher sehen also keinen Fehler, aber die Konsole meldet bei jedem Laden einen Fehler und lässt die Mismatches untergehen, die wirklich zählen.

## Reicht MotionConfig reducedMotion="user"?

Als globaler Standard ja: `reducedMotion="user"` ist die richtige Grundeinstellung und macht bei der Hydration keine Probleme. Motion liest die Einstellung, wenn jedes Motion-Element im Browser mountet, also steht nichts davon im Markup. Der Standardwert ist `"never"`: Motion ignoriert die Einstellung also, solange du sie nicht ausdrücklich einschaltest. Setz es einmal in einem Client-Provider:

```tsx
// app/providers.tsx
"use client";

import type { ReactNode } from "react";
import { MotionConfig } from "motion/react";

export function Providers({ children }: { children: ReactNode }) {
  return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
}
```

Die [Barrierefreiheits-Doku von Motion](https://motion.dev/docs/react-accessibility) beschreibt, was passiert: Transform- und Layout-Animationen werden abgeschaltet, andere Werte wie Opacity animieren weiter. Im Quellcode von 12.43 bekommt jeder Positionswert (`x`, `y`, `scale`, `rotate`, dazu `width`, `height`, `top` und `left`) `{ type: false }` und springt an sein Ziel, während jeder andere Wert seine Transition behält. Im Entwicklungsmodus loggt Motion außerdem einmal `You have Reduced Motion enabled on your device`; das ist erwartet und kein Bug.

Damit ist das meiste für Motion React Reduced Motion erledigt, mit zwei Lücken. Es kann nicht ändern, was du renderst (ein statisches Bild statt eines Videos mit Autoplay, eine Liste ohne Stagger), und ein Scale oder ein Slide, der direkt ans Ende springt, kann neben einem Fade wie ein Glitch wirken. Dafür liest du die Einstellung in der Komponente.

## Wie schreibst du einen usePrefersReducedMotion-Hook, der die Hydration übersteht?

Lies die Media Query über `useSyncExternalStore` und gib ihr einen Server-Snapshot, genau wie bei einem SSR-sicheren Media-Query-Hook:

```ts
// hooks/use-prefers-reduced-motion.ts
import { useSyncExternalStore } from "react";

const QUERY = "(prefers-reduced-motion: reduce)";

function subscribe(onChange: () => void) {
  const list = window.matchMedia(QUERY);
  list.addEventListener("change", onChange);
  return () => list.removeEventListener("change", onChange);
}

// false auf dem Server und während der Hydration, danach die echte Einstellung; aktualisiert sich bei Änderungen
export function usePrefersReducedMotion() {
  return useSyncExternalStore(subscribe, () => window.matchMedia(QUERY).matches, () => false);
}
```

React rendert während der Hydration den Server-Snapshot, also stimmen beide Seiten überein, und rendert direkt danach mit dem echten Wert neu. Komponenten, die später mounten, etwa ein Dialog, der sich per Klick öffnet, lesen beim ersten Render den echten Wert. Deshalb liefert unser `lib/motion` einen eigenen `usePrefersReducedMotion` auf Basis des [useMediaQuery](https://wingo-ui.com/components/use-media-query)-Hooks, statt den von Motion weiterzureichen. Der Beitrag zum [React use mobile Hook](https://wingo-ui.com/de/blog/react-use-mobile-hook-ssr) erklärt die Mechanik von `useSyncExternalStore` im Detail.

Der Kompromiss steht in der Tabelle: Was animiert, während die Seite hydriert, sieht `false` und spielt seine Eingangsanimation einmal vollständig ab. Lass Animationen beim Laden der Seite in CSS mit `motion-safe:` oder überlass sie `MotionConfig`, und nutz den Hook für das, was sich nach einem Klick öffnet.

## Was ersetzt Springs bei reduzierter Bewegung?

Ein kurzer Fade derselben Änderung. Die Animation ganz zu entfernen kostet Information: Ein Toast, der ohne Übergang in einer Ecke erscheint, auf die du gerade nicht schaust, geht leicht unter, und ein Dialog, der einfach aufpoppt, kann wie ein Darstellungsfehler wirken. Unsere Regel für jede Komponente:

- **Verschiebung streichen.** Kein Translate, Scale, Rotate oder Skew, kein Parallax, kein Stagger.
- **Opacity behalten, aber kürzer.** Ein linearer Fade von 150ms, der als Zustandswechsel ohne Bewegung ankommt.
- **Einblenden, was sich nur bewegt hat.** Ein Bottom Sheet hat keine eigene Opacity-Animation, also bekommt es bei reduzierter Bewegung eine.
- **Stoppen, was in Schleife läuft oder von selbst startet.** Autoplay, Laufschriften, animierte Hintergründe.

Im Code ist das eine Funktion. `reducePreset` in den [Motion Presets](https://wingo-ui.com/components/motion) entfernt Transform-Keys aus `initial`, `animate` und `exit` eines Presets, behält Opacity (und fügt sie hinzu, wenn das Preset keine hatte) und tauscht die Transition gegen 150ms linear. `useMotionPreset` wendet das über den Hook an:

```tsx
// components/saved-banner.tsx
"use client";

import { AnimatePresence, motion } from "motion/react";
import { useMotionPreset } from "@/lib/motion";

export function SavedBanner({ show }: { show: boolean }) {
  // normalerweise per Spring mit 4px Anstieg, bei reduzierter Bewegung ein Fade von 150ms
  const fade = useMotionPreset("fade");

  return (
    <AnimatePresence>
      {show && (
        <motion.p {...fade} className="rounded-2xl bg-success-soft px-4 py-3 text-sm text-success-soft-foreground">
          Änderungen gespeichert
        </motion.p>
      )}
    </AnimatePresence>
  );
}
```

Aktivier reduzierte Bewegung im Betriebssystem oder in den DevTools unter Rendering und spiel die Demo dann erneut ab. Die Spring-Bälle bleiben stehen, und die Listenzeilen blenden an Ort und Stelle ein, statt aufzusteigen:

> Live-Demo (Motion presets): Spiel die Liste mit jeder Spring erneut ab und schalte dann reduzierte Bewegung in den Systemeinstellungen ein: Die Bälle stoppen, und dieselben Zeilen blenden ohne Verschiebung an Ort und Stelle ein. Probier sie unter [Motion presets](https://wingo-ui.com/components/motion) aus und installier sie mit `npx wingo-ui@latest add motion`.

## Wie gehen Toast und Dialog mit reduzierter Bewegung um?

Sie folgen derselben Regel, mit ein paar Entscheidungen, die die Presets nicht allein treffen können. Der kostenlose [Toast](https://wingo-ui.com/components/toast) steigt normalerweise vom unteren Rand auf, stapelt ältere Toasts verkleinert hinter dem neuesten und gleitet beim Schließen Richtung Rand. Bei reduzierter Bewegung blendet ein neuer Toast an seinem endgültigen Platz ein, ein geschlossener blendet an Ort und Stelle aus, und der Stapel behält seine Abstände ohne die Skalierungsstufe. Der Toast bleibt eine Live Region, Screenreader sagen ihn also in beiden Fällen an:

> Live-Demo (Toast): Lös ein paar Toasts aus und versuch es dann mit eingeschalteter reduzierter Bewegung: Sie blenden an ihrem Platz ein, statt vom Rand aufzusteigen. Probier sie unter [Toast](https://wingo-ui.com/components/toast) aus und installier sie mit `npx wingo-ui@latest add toast`.

```tsx
// app/providers.tsx
"use client";

import type { ReactNode } from "react";
import { MotionConfig } from "motion/react";
import { Toaster } from "@/components/ui/toast";

export function Providers({ children }: { children: ReactNode }) {
  return (
    <MotionConfig reducedMotion="user">
      {children}
      <Toaster />
    </MotionConfig>
  );
}
```

Ruf `toast.success("Rechnung gesendet")` aus einer beliebigen Client-Komponente auf, und er wählt die passende Bewegung selbst.

Der [Dialog](https://wingo-ui.com/components/dialog) skaliert mit einer smooth Spring von 96% hoch, und ein Dialog, der aus ihm heraus geöffnet wird, schiebt den ersten nach hinten und oben. Bei reduzierter Bewegung blenden Backdrop und Karte nur ein, und der gestapelte Dialog tritt nicht zurück. Die einzige Animation, die wir ersatzlos streichen, ist der kleine 2%-Puls, den ein nicht schließbarer Dialog beim Klick daneben macht. Der Dialog bleibt trotzdem offen und der Fokus darin, und genau diese Rückmeldung zählt.

> Live-Demo (Dialog): Öffne den Dialog mit und ohne reduzierte Bewegung: Die Karte skaliert mit einer Spring hoch oder blendet an Ort und Stelle ein. Probier sie unter [Dialog](https://wingo-ui.com/components/dialog) aus und installier sie mit `npx wingo-ui@latest add dialog`.

Installier alle drei mit `npx wingo-ui@latest add motion toast dialog`. Alle drei sind kostenlos.

## Wie testest du reduzierte Bewegung?

An drei Stellen, von der schnellsten bis zur realistischsten:

- **Chrome DevTools.** Öffne das Command Menu, führ „Show Rendering“ aus und setz „Emulate CSS media feature prefers-reduced-motion“ auf `reduce`. Der Hook oben aktualisiert sich live, weil er die Query abonniert.
- **Playwright.** `browser.newContext({ reducedMotion: "reduce" })` lässt die ganze Suite mit eingeschalteter Einstellung laufen. Ergänz einen Test, der eine Seite hydriert und bei Konsolenfehlern fehlschlägt, dann kann der Mismatch von oben nicht zurückkommen.
- **Ein echtes Handy.** Schalte unter iOS „Bewegung reduzieren“ oder unter Android „Animationen entfernen“ ein und nutz die App eine Minute lang. Sheets, Toasts und Routenwechsel zeigen es am deutlichsten.

## Was solltest du als Nächstes tun?

Durchsuch deinen Code nach `useReducedMotion` und ersetz es durch einen `useSyncExternalStore`-Hook, ergänz `<MotionConfig reducedMotion="user">` in deinen Providern und verschieb Hover- und Lade-Animationen nach `motion-safe:`. Geh dann mit eingeschalteter Einstellung durch die App und prüf, ob jede Zustandsänderung noch sichtbar ist. Weitere Beiträge zu diesem Stack findest du unter [Next.js und Tailwind CSS](https://wingo-ui.com/de/blog/category/nextjs-tailwind), darunter den Pillar-Artikel zu [Tailwind v4 Design Tokens](https://wingo-ui.com/de/blog/tailwind-v4-design-tokens), der Motion-Tokens neben Farben behandelt. Unser Beitrag zu [Framer Motion Spring Presets](https://wingo-ui.com/de/blog/framer-motion-spring-presets) listet die vier Springs, die dieser Fallback ersetzt, und der [Fix für den next-themes Hydration Error](https://wingo-ui.com/de/blog/next-themes-hydration-error) wendet dieselbe Idee mit dem Server-Snapshot auf den Theme-Toggle an.

## Komponenten in diesem Artikel

- [Motion presets](https://wingo-ui.com/components/motion) (Free): The shared springs, enter and exit presets, stagger helpers and reduced-motion hooks every animated component uses. Installation: `npx wingo-ui@latest add motion`
- [Toast](https://wingo-ui.com/components/toast) (Free): React toast notifications: mount one Toaster and call toast() from anywhere, with stacking, swipe to dismiss and a pause while you read. Installation: `npx wingo-ui@latest add toast`
- [Dialog](https://wingo-ui.com/components/dialog) (Free): A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling. Installation: `npx wingo-ui@latest add dialog`

## Häufige Fragen

### Warum verursacht useReducedMotion einen Hydration Mismatch?

useReducedMotion von Motion liefert auf dem Server null, weil es dort keine Media Query gibt, und beim ersten Client-Render die echte Antwort des Browsers. Wählt dieser Wert einen Startstil oder ein Attribut, passen Server-HTML und Hydration-Render nicht zusammen, und React meldet im Entwicklungsmodus einen Mismatch.

### Verursacht MotionConfig reducedMotion="user" Hydration-Fehler?

Nein. Motion liest die Einstellung erst, wenn jedes Motion-Element im Browser mountet, und nichts davon steht im gerenderten Markup. Server-HTML und Hydration-Render stimmen also überein. Transforms und Layout-Animationen springen direkt ans Ende, Opacity und Farben animieren weiter.

### Wie nutze ich prefers-reduced-motion in Tailwind CSS?

Stell Animationen und Transitions das Präfix motion-safe: voran, das zu @media (prefers-reduced-motion: no-preference) kompiliert, oder schalte sie mit motion-reduce: ab. Beides ist reines CSS und greift deshalb schon beim ersten Paint, ganz ohne JavaScript.

### Sollte reduzierte Bewegung alle Animationen abschalten?

Nein. Die Einstellung bittet um weniger Bewegung, und Feedback bleibt wichtig: Halte Opacity- und Farbwechsel kurz und streich Verschiebung, Skalierung, Parallax und Autoplay. Ein Toast, der einblendet, sagt der Person trotzdem, dass etwas passiert ist.

### Verlangt die WCAG, dass ich prefers-reduced-motion respektiere?

Erfolgskriterium 2.3.3 Animation from Interactions auf Stufe AAA verlangt, dass sich durch Interaktion ausgelöste Bewegung abschalten lässt, sofern sie nicht wesentlich ist. Schon Kriterium 2.2.2 auf Stufe A fordert, dass sich Bewegung, die von selbst startet und länger als fünf Sekunden dauert, pausieren, stoppen oder ausblenden lässt.

---

Quelle: https://wingo-ui.com/de/blog/prefers-reduced-motion-react
