Motion Foundations
Die Basisschicht des Bewegungssystems. Definiert jeden Wert, jede Einschränkung und jede Regel, die von nachgelagerten Skills (motion-patterns, motion-advanced) geerbt werden. Laden Sie diesen Skill, bevor Sie mit Animationsarbeiten beginnen.
Wann aktivieren
- Wenn Sie eine animierte Komponente von Grund auf neu erstellen
- Beim Einrichten von Tokens, Feder-Voreinstellungen oder Beschleunigungswerten
- Bei der Implementierung von
prefers-reduced-motion-Unterstützung - Beim Debuggen von Hydrierungsinkonsistenzen aufgrund von Animations-Startzuständen
- Bei der Bewertung, ob eine Animation überhaupt existieren sollte
Ergebnisse
Dieser Skill erzeugt:
- Ein gemeinsames
motionTokens-Objekt (Dauer, Beschleunigung, Distanz, Skalierung) - Eine gemeinsame
springs-Voreinstellungskarte (5 benannte Konfigurationen) - Eine
shouldAnimate()-Schranke, die von allen Komponenten verwendet wird - Barrierefreie Animationsstandards mittels
useReducedMotion - SSR-sichere Startzustände ohne Hydrierungswarnungen
Prinzipien
Bewegung muss mindestens eines der folgenden Ziele erfüllen, sonst muss sie entfernt werden:
- Aufmerksamkeit lenken
- Zustand kommunizieren
- Räumliche Kontinuität bewahren
Reaktionsfähigkeit hat immer Vorrang vor Geschmeidigkeit. Eine Animation mit 60 fps, die zu Eingabeverzögerungen führt, ist schlimmer als keine Animation.
Regeln
Diese sind nicht verhandelbar. Sie gelten für jede Komponente im System.
- Nur
motion/reactverwenden. Niemals ausframer-motionimportieren. Niemals beide im selben Baum mischen. initialmuss mit der Serverausgabe übereinstimmen. Wenn der Serveropacity: 1rendert, muss dieinitial-Eigenschaft ebenfallsopacity: 1sein. Keine Ausnahmen.- Reduzierte Bewegung überschreibt alles. Wenn
useReducedMotion()truezurückgibt oderprefersReducedtrueist, werden alle Transformationen deaktiviert. Nur Opazitäts-Übergänge bei ≤ 0,2 Sekunden sind als einziger Fallback erlaubt. - Layout-Eigenschaften niemals animieren.
width,height,top,left,margin,paddingsind inanimateverboten. Verwenden Sie ausschließlichtransformundopacity. - Alle Token-Werte stammen aus
motionTokens. Hartcodierte Dauern und Beschleunigungen in Komponentendateien sind verboten. - Alle Feder-Konfigurationen stammen aus der
springs-Karte. Inlinestiffness/damping-Werte sind verboten. "use client"ist erforderlich in jeder Datei, die ausmotion/reactimportiert.- Niemals
windowodernavigatorauf Modulebene lesen. Immer mittypeof window !== "undefined""absichern.
Entscheidungshilfe
Eine Dauer auswählen
| Token | Verwenden, wenn |
|---|---|
instant | Tooltip ein-/ausblenden, Fokusring, Badge-Update |
fast | Button-Feedback, Icon-Wechsel, Chip-Umschaltung |
normal | Modal öffnen, Karte erweitern, Seitenelement eintreten |
slow | Hero-Eingang, seitenweiter Übergang |
crawl | Bewusstes Storytelling; sparsam verwenden |
Eine Feder auswählen
| Voreinstellung | Verwenden, wenn |
|---|---|
snappy | Standard-UI — Buttons, Chips, Nav-Elemente |
gentle | Karten, Modals, Panels, die sanft landen |
bouncy | Verspielte Momente — leere Zustände, Einführung |
instant | Tooltips, Popovers, Dropdowns |
release | Zieh-Loslassen — natürliches physikalisches Gefühl |
Wann Animation vollständig deaktivieren
Deaktivieren (lassen Sie shouldAnimate() false zurückgeben), wenn:
prefersReducedtrueistisLowEndtrueist und die Animation nicht wesentlich ist- Das Element außerhalb des Bildschirms ist und niemals in den Viewport gelangt
- Die Animation rein dekorativ ist und keinen UX-Zweck hat
Kernkonzepte
Token-System
// lib/motion-tokens.ts
export const motionTokens = {
duration: {
instant: 0.08,
fast: 0.18,
normal: 0.35,
slow: 0.6,
crawl: 1.0,
},
easing: {
smooth: [0.22, 1, 0.36, 1],
sharp: [0.4, 0, 0.2, 1],
bounce: [0.34, 1.56, 0.64, 1],
linear: [0, 0, 1, 1],
},
distance: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 48,
},
scale: {
subtle: 0.98,
press: 0.95,
pop: 1.04,
},
}
export const springs = {
snappy: { type: "spring", stiffness: 300, damping: 30 },
gentle: { type: "spring", stiffness: 120, damping: 14 },
bouncy: { type: "spring", stiffness: 400, damping: 10 },
instant: { type: "spring", stiffness: 600, damping: 35 },
release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}
Laufzeit-Flags
// lib/motion-config.ts
export const motionConfig = {
isLowEnd() {
return (
typeof navigator !== "undefined" &&
navigator.hardwareConcurrency <= 4
)
},
prefersReduced() {
return (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
)
},
shouldAnimate({ essential = false } = {}) {
if (this.prefersReduced()) return false
if (!essential && this.isLowEnd()) return false
return true
},
duration() {
return this.isLowEnd() || this.prefersReduced()
? motionTokens.duration.instant
: motionTokens.duration.normal
},
}
Barrierefreiheit
Prioritätsreihenfolge (höchste zu niedrigste):
prefers-reduced-motion: reduce— deaktiviert alle Transformationen, begrenzt Opazitäts-Übergänge auf ≤ 0,2s- Erkennung von Geräten mit niedriger Leistung — reduziert die Dauer, entfernt nicht wesentliche Animationen
- Design-Präferenz — alles andere
Bewegung muss sich anmutig verschlechtern. Sie darf niemals abrupt verschwinden, was zu Layout-Verschiebungen oder Orientierungsverlust führen würde.
// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"
export function useSafeMotion(fullY: number = 16) {
const reduce = useReducedMotion()
return {
initial: { opacity: 0, y: reduce ? 0 : fullY },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: reduce ? 0 : -fullY },
}
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition { transition: opacity 0.15s; }
.motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
SSR / Hydrierungs-Sicherheit
Regel: initial muss immer mit dem übereinstimmen, was der Server rendert.
// FALSCH — Server rendert opacity:1, aber initial sagt 0 → Hydrierungskonflikt
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// RICHTIG — AnimatePresence verwenden oder auf Client-Mount verschieben
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
Codebeispiele
Durchgängig: Tokens + Federn + Barrierefreiheit + SSR-Schutz
// components/fade-in-card.tsx
"use client"
import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"
interface FadeInCardProps {
children: React.ReactNode
delay?: number
}
export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
// SSR-Schutz — initial muss mit Serverausgabe übereinstimmen (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Barrierefreiheit — deaktiviert Transformation, wenn reduzierte Bewegung bevorzugt wird
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Geräteschranke — Animation auf schwacher Hardware überspringen
if (!motionConfig.shouldAnimate() || !mounted) {
return <div>{children}</div>
}
return (
<motion.div
initial={safeMotion.initial}
animate={safeMotion.animate}
exit={safeMotion.exit}
transition={{
...springs.gentle,
delay,
}}
whileHover={{ scale: motionTokens.scale.pop }}
whileTap={{ scale: motionTokens.scale.press }}
>
{children}
</motion.div>
)
}
Einschränkungen / Nicht-Ziele
Dieser Skill behandelt nicht:
- UI-Komponentenmuster (Button, Modal, Staffelung) → siehe
motion-patterns - Drag, Gesten, SVG, Textanimationen, benutzerdefinierte Hooks → siehe
motion-advanced - CSS-only-Animationen oder Tailwind
animate-*-Klassen ohnemotion/react - Animationsbibliotheken von Drittanbietern (GSAP, anime.js, etc.)
- Bewegungsdesignentscheidungen (wann animieren, was hervorheben) — das ist eine Designfrage, keine Codeeinschränkung
Anti-Patterns
| Anti-Pattern | Verletzte Regel | Korrektur |
|---|---|---|
import { motion } from "framer-motion" | Regel 1 | motion/react verwenden |
initial={{ opacity: 0 }} bei SSR-Komponente | Regel 2 | Mount-Schutz hinzufügen |
useReducedMotion-Überprüfung auslassen | Regel 3 | useSafeMotion-Hook verwenden |
animate={{ width: "100%" }} | Regel 4 | scaleX-Transformation stattdessen verwenden |
transition={{ duration: 0.4 }} inline | Regel 5 | motionTokens.duration.normal verwenden |
{ stiffness: 300, damping: 30 } inline | Regel 6 | springs.snappy verwenden |
Fehlende "use client"-Direktive | Regel 7 | Am Dateianfang hinzufügen |
navigator.hardwareConcurrency auf Modulebene | Regel 8 | In typeof navigator !== "undefined" einwickeln |
Verwandte Skills
motion-patterns— verbraucht die hier definierten Tokens und Federn, um Button-, Modal-, Staffelungs-, Seitenübergangs- und Scroll-Muster zu erstellen. Definiert keine Werte neu.motion-advanced— verbraucht die hier definierten Tokens und Federn für Drag-, SVG-, Text- und Gestenmuster. FügtuseAnimate-Sequenzen und benutzerdefinierte Hooks auf dieser Grundlage hinzu.