Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Next.js und Tailwind CSS
    4. Prefers Reduced Motion React ohne Hydration Mismatch
    Read in English
    1. Blog
    2. Next.js und Tailwind CSS
    3. Prefers Reduced Motion React ohne Hydration Mismatch
    Next.js und Tailwind CSS
    Next.js und Tailwind CSS

    Prefers Reduced Motion React ohne Hydration Mismatch

    SR
    Serban Rusu · Gründer von Wingo UI
    13. Okt. 2026 · 8 Min. Lesezeit

    Auf dieser Seite

    0%
    1. Worum bittet prefers-reduced-motion?
    2. Wie gehst du in Tailwind mit prefers-reduced-motion um?
    3. Warum verursacht useReducedMotion einen Hydration Mismatch?
    4. Reicht MotionConfig reducedMotion="user"?
    5. Wie schreibst du einen usePrefersReducedMotion-Hook, der die Hydration übersteht?
    6. Was ersetzt Springs bei reduzierter Bewegung?
    7. Wie gehen Toast und Dialog mit reduzierter Bewegung um?
    8. Wie testest du reduzierte Bewegung?
    9. Was solltest du als Nächstes tun?
    10. Häufige Fragen
      1. Warum verursacht useReducedMotion einen Hydration Mismatch?
      2. Verursacht MotionConfig reducedMotion="user" Hydration-Fehler?
      3. Wie nutze ich prefers-reduced-motion in Tailwind CSS?
      4. Sollte reduzierte Bewegung alle Animationen abschalten?
      5. Verlangt die WCAG, dass ich prefers-reduced-motion respektiere?

    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.

    Veröffentlicht am 13. Okt. 2026

    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 (opens in a new tab). 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 (opens in a new tab) 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 (opens in a new tab), 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 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:

    AnsatzReduzierte Bewegung anReduzierte Bewegung ausFolgt einer Änderung der Einstellung
    Motion useReducedMotion() wählt initialA tree hydrated but some attributes ... didn't matchSauber als Boolean; in ein Attribut gerendert ist es null gegen false, ein Mismatch bei jedem BesuchNein, einmal in den State gelesen
    useSyncExternalStore-Hook, Server-Snapshot falseSauber; eine Eingangsanimation, die während der Hydration läuft, spielt einmal vollständig abSauberJa
    <MotionConfig reducedMotion="user">Sauber; Transforms springen ans Ende, Opacity wird weiter animiertSauberFü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 (opens in a new tab) 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-Hooks, statt den von Motion weiterzureichen. Der Beitrag zum React use mobile Hook 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 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:

    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
    $ npx wingo-ui@latest add motion
    FreeDoku: Motion presets

    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 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:

    Lös ein paar Toasts aus und versuch es dann mit eingeschalteter reduzierter Bewegung: Sie blenden an ihrem Platz ein, statt vom Rand aufzusteigen
    $ npx wingo-ui@latest add toast
    FreeDoku: 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 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.

    Öffne den Dialog mit und ohne reduzierte Bewegung: Die Karte skaliert mit einer Spring hoch oder blendet an Ort und Stelle ein
    $ npx wingo-ui@latest add dialog
    FreeDoku: 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, darunter den Pillar-Artikel zu Tailwind v4 Design Tokens, der Motion-Tokens neben Farben behandelt. Unser Beitrag zu Framer Motion Spring Presets listet die vier Springs, die dieser Fallback ersetzt, und der Fix für den next-themes Hydration Error wendet dieselbe Idee mit dem Server-Snapshot auf den Theme-Toggle an.

    Komponenten in diesem Artikel

    • Motion presets

      The shared springs, enter and exit presets, stagger helpers and reduced-motion hooks every animated component uses.

      Free
    • Toast

      React toast notifications: mount one Toaster and call toast() from anywhere, with stacking, swipe to dismiss and a pause while you read.

      Free
    • Dialog

      A React modal dialog for focused tasks: a spring-scaled card on desktop and a bottom sheet on phones, with nesting and scrolling.

      Free

    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.

    • React
    • Barrierefreiheit
    • Animation
    • Next.js
    • Tailwind CSS

    Teilen

    SR

    Über den Autor

    Serban Rusu

    Gründer von Wingo UI

    Serban Rusu hat Wingo UI gegründet. Er baut die Komponenten, die CLI und den MCP-Server und schreibt über React-UIs für Smartphones und KI-Agents.

    Mehr von Serban
    Weiterlesen

    Ähnliche Artikel

    Alle Artikel
    Vergleiche und Alternativen

    Chakra UI vs shadcn 2026: Style Props oder Tailwind?

    Chakra UI vs shadcn im Oktober 2026: Style Props auf Emotion oder Tailwind-Code, der dir gehört. Theming, Turbopack, RSC und Smartphones, mit Codebeispielen.

    SRSerban Rusu·16. Okt. 2026·10 Min. Lesezeit
    Next.js und Tailwind CSS

    Next.js Component Library im App Router einrichten

    Next.js Component Library im App Router einrichten, getestet mit Next.js 16.4: use client, Tokens in globals.css, Dark Mode ohne Flackern, Updates per CLI.

    SRSerban Rusu·13. Okt. 2026·8 Min. Lesezeit
    Next.js und Tailwind CSS

    shadcn Vite Setup mit Tailwind CSS v4 und React 19

    shadcn Vite Setup, getestet mit Vite 8 und TypeScript 6: Tailwind-v4-Plugin, @-Alias in tsconfig.app.json, Dark Mode ohne Next.js, dann Komponenten hinzufügen.

    SRSerban Rusu·13. Okt. 2026·7 Min. Lesezeit
    Newsletter

    Neue Artikel per E-Mail

    Neue Guides, Tutorials und Vergleiche aus dem Wingo UI Blog, sobald sie erscheinen. Die meisten Artikel sind auf Englisch.

    Kein Spam. Du kannst dich jederzeit abmelden.

    WingoUI

    Animierte, konfigurierbare, mobile-first React-Komponenten und Blöcke. Kopier den Quellcode, pass ihn an und lass deinen Coding-Agenten damit bauen.

    Produkt

    • Komponenten
    • Vorlagen
    • Preise
    • KI-Agenten
    • Theme-Generator

    Ressourcen

    • Doku
    • Blog
    • Changelog
    • Changelog-RSS
    • llms.txt

    Rechtliches

    • AGB
    • Datenschutz
    • Rückerstattung
    • Lizenz
    • Impressum

    Komponenten-Kategorien

    • Buttons & Actions
    • Inputs
    • Forms
    • Navigation
    • Overlays
    • Feedback
    • Data Display
    • Tables & Lists
    • Charts & Stats
    • Layout
    • Media
    • AI Kit
    • Text & Effects
    • Mobile
    • Commerce
    • Marketing Sections
    • Blocks
    • Hooks & Utilities

    © 2026 Wingo UI

    Wingo UI