React Animation
Die Plugin-Installation enthält kein docs/react/ Verzeichnis. Wenn der Pfad docs/... in dieser Datei nicht geöffnet werden kann, hänge denselben Pfad an https://raw.githubusercontent.com/joo6077/claude-plugins/main/ an und versuche, die Datei zu lesen. Wenn das auch nicht funktioniert, schreibe, dass die Datei nicht gelesen werden konnte, und erfinde keinen Inhalt.
Fallstricke
-
Null-Bibliotheken-Prinzip – absolut keine Ausnahmen: Motion (framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css sind verboten (Installation und Import). Wenn Import-Anweisungen für diese Bibliotheken in der Codebasis vorhanden sind, gibt
/react-auditeinen Build-Fehler aus. Auch auf Anfrage des Benutzers hin eine alternative Implementierung vorschlagen und die Verwendung der Bibliothek verweigern. -
Immer mit der niedrigsten Stufe beginnen: Bei einer "Animation"-Anfrage wird oft fälschlicherweise sofort Stufe 3 verwendet. Befolge die Bewertungsregeln und prüfe zuerst, ob Stufe 1 möglich ist. Nur wenn nicht, gehe zu einer höheren Stufe über.
-
tailwindcss-animatePlugin prüfen: Erweiterte Utility-Klassen wieanimate-in,fade-in-*,slide-in-from-*benötigentailwindcss-animate. Es ist standardmäßig in shadcn/init-Projekten enthalten. Wenn nicht vorhanden, führepnpm add -D tailwindcss-animateaus und weise auf die Plugin-Registrierung intailwind.config.tshin. -
Eindeutigkeit von
view-transition-name: Wenn zwei Elemente gleichzeitig denselben Namen haben, tritt ein "duplicate name"-Fehler auf. Verwende unbedingt dynamische IDs (z. B.view-transition-name: task-${item.id}), um die Eindeutigkeit zu gewährleisten. -
pointercancel-Behandlung erforderlich: Wenn daspointercancel-Ereignis nicht behandelt wird, kann der Drag-Zustand bei Tab-Wechsel, Fokus auf eine andere App oder beim Unmounten von Elementen undicht werden. Setze imonPointerCancel-Handler den FSM unbedingt aufidlezurück. -
Fehlendes
touch-action: noneverhindert Mobile-Drag: Füge zu pointer-event-basierten Drag-Elementen unbedingt die Tailwind-Klassetouch-nonehinzu, um das standardmäßige Scrollen/Zoomen des Browsers zu blockieren. -
Zustand-Drag-Store ist eine globale, einzelne Instanz: Erstelle keinen separaten Drag-Store pro Feature.
src/presentation/shared/stores/drag-store.tswird von allen Features gemeinsam genutzt. -
prefers-reduced-motionnicht angewendet: In allen Stufen müssen Animationen mit@media (prefers-reduced-motion: reduce)oder der Tailwind-Variantemotion-reduce:*deaktiviert werden. Bei Fehlen gibt/react-auditeine Warnung aus. -
Animationen sind nur für die Präsentationsschicht: Importiere keine pointer events, view transitions oder den Zustand-Drag-Store in den Schichten
domain/oderdata/. Animationsbezogener Code befindet sich ausschließlich insrc/presentation/. -
Firefox unterstützt keine
types-Parameter: Die Optiontypesindocument.startViewTransition({ update, types: [...] })wird Stand 2026 nur stabil in Chromium-basierten Browsern unterstützt. Behandle den Fallback imwithViewTransition-Wrapper mit try/catch. -
Browser-Unterstützung für scroll-gesteuerte Animationen (Stand 13.08.2026):
animation-timeline: scroll()/scroll-timelinehaben laut Can I Use eine globale Abdeckung von 85,43% – Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+. Die Aussage, dass Firefox hinter einem Flag steckt, ist veraltet. Allerdings istview()laut MDN noch nicht Baseline. Daher bleibt die Richtlinie bestehen, diese mit@supports (animation-timeline: scroll())/@supports (animation-timeline: view())zu umschließen und bei fehlender Unterstützung auf statische Styles zurückzugreifen. -
Nutzung von
view-transition-name: match-element(Chrome 137+): Anstatt Dutzenden von Elementen manuell eindeutige Namen zu geben, kann der CSS-Wertview-transition-name: match-elementverwendet werden, damit der Browser die Elemente automatisch zuordnet. Da dies Stand Q2 2026 jedoch Chromium-exklusiv ist, einen Fallback-Pfad beibehalten. -
Bewegung wird nicht durch statischen Code nachgewiesen – Nachweis vor Abschlusserklärung erforderlich (E2): Das Ergebnis dieses Skills kann seine Existenz nur durch Abspielen bestätigen. Die Tatsache, dass eine
animate-*-Klasse vorhanden ist oder der CodestartViewTransitionaufruft, ist ein R3 (statischer) Nachweis und kein Beweis dafür, dass die Animation tatsächlich abgespielt wurde – nicht definierte Keyframes, dauerhaft zutreffendesprefers-reduced-motion, Abschneiden durchoverflow: hiddendes übergeordneten Elements und das Betreten des Fallbacks eines nicht unterstützten Browsers führen alle still dazu, dass "nichts passiert". Fülle kurz vor Fertigstellung die Checkliste aus §4 vonreact-kit/references/render-evidence-protocol.md. Bei der Überprüfung durch Snapshot-Vergleich markiere zwei Zeitpunkte (vorher/nachher) – ein einzelner Standbild-Frame liefert für jede Bewegung das gleiche Ergebnis und ist daher kein Orakel (§3 d). Wenn kein Nachweis erbracht werden kann, füge[Ungeprüft]hinzu und fülle die vier Felder (Hindernis · versuchte Umgehung · Grund für fehlende Kontrolle · Befehl zur erneuten Überprüfung – Protokoll §2) aus, um als teilweise abgeschlossen zu melden.Führe keine Animationsbibliothek ein, um einen Nachweis zu erbringen. Die Library Policy (Fallstrick #1) steht über dieser Regel und wird durch keine Überprüfungsbequemlichkeit gelockert.
-
Die Basisaufnahme wird vor der Bearbeitung gemacht – Führe §1 Schritt 0 und den Vergleichswiederholungszyklus aus §2, Punkt 1 von
react-kit/references/render-evidence-protocol.mdvor der ersten Bearbeitung aus und hinterlasse das Ergebnis (Wiederholung, Konventionstabelle, aktuellen Screenshot-Pfad und zu ändernde Markierung) in der Antwort. Nach der Bearbeitung kann der Zustand vor der Bearbeitung nicht erneut aufgenommen werden. -
Tier 2 wird nach Update-Typ unterschieden –
<ViewTransition>in React 19.3 ist eine stabile API (Hinzugefügt am 26.09.2026):<ViewTransition>, das vonreactimportiert wird, ist in 19.3 keine experimentelle API mehr, sondern eine stabile API. Es wird ausgelöst, wenn sich der Stil von umschlossenen Kindern durch als Transition markierte Aktualisierungen (startTransition-Aktualisierung, Suspense Reveal,useDeferredValue) ändert oder sie gemountet/unmountet werden, und funktioniert nur im DOM (Quelle: https://react.dev/blog/2026/09/09/react-19-3: „We shared it as an experimental API last year, and in 19.3 it's stable and ready to use."). Verwende daher<ViewTransition>für als Transition markierte React-Zustandsaktualisierungen und den §3.1withViewTransition-Wrapper für andere DOM-Aktualisierungen (nicht als Transition markierte synchrone Aktualisierungen, DOM-Manipulationen außerhalb von React). Entferne den Wrapper nicht. Da es sich um eine Komponente imreact-Paket handelt, verstößt sie nicht gegen das Null-Bibliotheken-Prinzip (#1). Wenn dasreact-Paket des Projekts älter als 19.3 ist, verwende nur den Wrapper.
Prozess
1. Automatische Tier-Bewertung
Wenn kein --tier-Flag gesetzt ist, analysiere die Benutzeranfrage und wähle die niedrigste Stufe aus.
| Schlüsselwort / Szenario | Bewertete Stufe |
|---|---|
| "Einblenden", "Hochschieben", "Skalieren", "Pulsieren", "Hüpfen", "Hover-Effekt", "Deckkraft", "Schimmern", "Eingangsanimation" | T1 |
| "Skeleton-Loading → Fertig-Übergang", "Button Hover", "Accordion", "Modal Öffnen/Schließen", "Zustandsänderung" | T1 |
| "Scroll-Animation", "scroll-gesteuert", "Parallax", "Scroll-Fortschrittsbalken", "scroll-basiert" | T1 |
| "Seitenübergang", "geteiltes Element", "Grid zu Board", "View-Übergang", "Routenübergang", "DOM-Strukturänderung" | T2 |
| "scroll-ausgelöst", "Beim Scroll-Kreuzungspunkt auslösen", "Animation an bestimmter Position starten" | T2 |
| "Drag & Drop", "Sortierbare Liste", "sortierbar", "Kanban", "Ziehen", "Ablegen", "Geste", "Pfeil-Verbindungslinie" | T3 |
Wenn die Grenze zwischen T2/T3 unklar ist, konsultiere den animation-architect-react-Agenten.
3-Stufen-Zusammenfassung:
| Stufe | Werkzeug | Anwendungsszenario | Schwierigkeitsgrad |
|---|---|---|---|
| T1 | Tailwind animate-* + CSS @keyframes + scroll-gesteuert | Zustandsänderung, Hover, einfache Schleife, Scroll-Kopplung | Niedrig |
| T2 | View Transitions API (withViewTransition-Wrapper) · React 19.3 <ViewTransition> (nur als Transition markierte Aktualisierungen – Fallstrick 15) | View-/Routenübergang, geteiltes Element, FLIP | Mittel |
| T3 | Pointer Events + FSM + requestAnimationFrame | Drag & Drop, Geste, SVG-Verbindungslinie | Hoch |
2. Stufe 1 – Tailwind + CSS-Implementierung
2.1 Tailwind integrierte Utilities
| Kategorie | Utility | Beispiel |
|---|---|---|
| Übergang | transition-transform, transition-colors, transition-opacity | hover:scale-105 transition-transform duration-200 |
| Dauer | duration-75~duration-1000 | duration-300 |
| Beschleunigung | ease-linear, ease-in, ease-out, ease-in-out | ease-in-out |
| Animation (integriert) | animate-spin, animate-ping, animate-pulse, animate-bounce | Laden, Benachrichtigung |
| Verzögerung | delay-75~delay-1000 | Staffel-Effekt |
2.2 tailwindcss-animate Erweiterungs-Utilities
Enter/Exit-Animationen bereitgestellt durch das tailwindcss-animate-Plugin:
animate-in/animate-outfade-in-*,fade-out-*slide-in-from-top/bottom/left/right-*,slide-out-to-*zoom-in-*,zoom-out-*
Kombiniere mit data-[state=...] Attributselektoren für zustandsbasierte Animationen:
// Beispiel Drawer öffnen/schließen
<div
data-state={open ? 'open' : 'closed'}
className="
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left
duration-300
motion-reduce:transition-none motion-reduce:animate-none
"
>
Drawer-Inhalt
</div>
2.3 Direkte CSS @keyframes-Deklaration
Deklariere benutzerdefinierte Animationen, die nicht von Tailwind-Utilities abgedeckt werden, im @theme-Block von globals.css:
/* src/presentation/styles/globals.css */
@import "tailwindcss";
@theme {
--animate-wiggle: wiggle 1s ease-in-out infinite;
--animate-shimmer: shimmer 2s linear infinite;
@keyframes wiggle {
0%, 100% { transform: rotate(-3deg); }
50% { transform: rotate(3deg); }
}
@keyframes shimmer {
from { background-position: -200% 0; }
to { background-position: 200% 0; }
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Verwendung: <div className="animate-wiggle">...</div>
2.4 Beispiele für Stufe 1-Szenarien
Fade-in-Eingang:
<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
Fade-in beim Seitenladen
</div>
Hover-Scale-Button:
<button className="transition-transform duration-200 hover:scale-105 active:scale-95 motion-reduce:transition-none">
Knopf
</button>
Gestaffelter Listeneingang:
{items.map((item, i) => (
<div
key={item.id}
className="animate-in slide-in-from-bottom-4 fade-in-0 motion-reduce:animate-none"
style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'backwards' }}
>
{item.name}
</div>
))}
2.5 Scroll-gesteuerte Animationen (CSS-nativ)
Implementiere Animationen, die an die Scroll-Position gekoppelt sind, ohne JavaScript mit CSS animation-timeline. Stand 13.08.2026 werden scroll() / scroll-timeline in Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ unterstützt (Can I Use global 85,43%), und view() ist laut MDN noch nicht Baseline. Umschließe beide Fälle mit @supports für einen gracevollen Fallback in nicht unterstützten Browsern.
Scroll-Fortschrittsbalken:
/* globals.css */
@keyframes progress-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
<div
className="fixed top-0 left-0 h-1 w-full origin-left bg-primary motion-reduce:hidden"
style={{
animation: 'progress-grow linear',
animationTimeline: 'scroll(root block)',
}}
/>
Fade-in beim Elementeintritt (View Timeline):
@keyframes fade-slide-in {
from { opacity: 0; transform: translateY(2rem); }
to { opacity: 1; transform: translateY(0); }
}
@supports (animation-timeline: view()) {
.scroll-reveal {
animation: fade-slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
<section className="scroll-reveal motion-reduce:opacity-100">
Beim Scrollen erscheinender Inhalt
</section>
Hinweis: Scroll-ausgelöste Animationen (Chrome 145, 2026) sind ein separates Konzept, das beim Kreuzen eines bestimmten Scroll-Offsets eine zeitbasierte Animation auslöst. Nicht mit scroll-gesteuert verwechseln. Scroll-ausgelöste Animationen werden als Kandidat für Stufe 2 eingestuft.
3. Stufe 2 – View Transitions API-Implementierung
Same-Document View Transitions sind laut MDN Baseline 2025, laut Can I Use global 90,2% – Chrome/Edge 111+, Safari 18+, Firefox 144+ (Stand 13.08.2026). Automatische FLIP-Animation bei DOM-Änderungen innerhalb einer SPA.
Hingegen sind @view-transition-basierte Cross-Document / MPA-Übergänge und einige Detailfunktionen noch limited availability. Verwende bei Routenübergängen standardmäßig den Same-Document-Pfad und behalte die Feature-Erkennung + Fallback bei (§6 Standardabdeckungslücke 8).
3.1 withViewTransition-Wrapper erstellen
Erstelle die Datei, falls nicht vorhanden:
// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
updateDOM: () => void,
options?: { types?: string[] },
): void {
// prefers-reduced-motion respektieren
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
updateDOM()
return
}
// Feature-Erkennung (Fallback für alte Browser)
if (typeof document.startViewTransition !== 'function') {
updateDOM()
return
}
// Verzweigung für Unterstützung des types-Parameters (Firefox-Initialimplementierung nicht unterstützt)
try {
if (options?.types && options.types.length > 0) {
document.startViewTransition({ update: updateDOM, types: options.types })
} else {
document.startViewTransition(updateDOM)
}
} catch {
updateDOM()
}
}
3.2 view-transition-name CSS für geteilte Elemente
Auch wenn sich die DOM-Struktur ändert, als "gleiches Element" identifizieren und sanft verschieben:
// Eindeutigkeit durch dynamische ID gewährleisten (doppelte Namen verboten)
<div
className="rounded-lg border p-4"
style={{ viewTransitionName: `task-${task.id}` }}
>
{task.title}
</div>
3.3 Grid ↔ Board-View-Übergang (repräsentatives Szenario)
// src/presentation/features/tasks/components/task-switcher.tsx
import { withViewTransition } from '@/presentation/shared/lib/view-transition'
import { useTaskStore } from '../store'
export function TaskViewSwitcher() {
const viewMode = useTaskStore((s) => s.viewMode)
const setViewMode = useTaskStore((s) => s.setViewMode)
return (
<button
onClick={() => {
withViewTransition(() => {
setViewMode(viewMode === 'grid' ? 'board' : 'grid')
})
}}
>
{viewMode === 'grid' ? 'Zur Board-Ansicht' : 'Zur Grid-Ansicht'}
</button>
)
}
export function TaskList({ tasks }: { tasks: Task[] }) {
const viewMode = useTaskStore((s) => s.viewMode)
return (
<div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'flex gap-4 overflow-x-auto'}>
{tasks.map((task) => (
<div
key={task.id}
className="rounded-lg border p-4"
style={{ viewTransitionName: `task-${task.id}` }}
>
{task.title}
</div>
))}
</div>
)
}
3.4 CSS-Übergangs-Feinabstimmung
/* globals.css – Übergangsdauer + Beschleunigung */
::view-transition-group(*) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Individuelle Anpassung für bestimmte Elemente */
::view-transition-group(hero-image) {
animation-duration: 0.6s;
}
/* Sofortiger Übergang bei reduzierter Bewegung */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*) {
animation-duration: 0.01ms;
}
}
4. Stufe 3 – Implementierung von benutzerdefinierten Pointer-Primitiven
Direkte Implementierung ohne Bibliotheken unter Verwendung der Pointer Events API + Zustandsmaschine (FSM).
4.1 useDrag-Hook erstellen
// src/presentation/shared/hooks/use-drag.ts
import { useCallback, useEffect, useReducer, useRef } from 'react'
// FSM 3 Zustände: idle / dragging / dropping
type DragState =
| { kind: 'idle' }
| { kind: 'dragging'; startX: number; startY: number; offsetX: number; offsetY: number; pointerId: number }
| { kind: 'dropping'; offsetX: number; offsetY: number }
type DragAction =
| { type: 'start'; x: number; y: number; pointerId: number }
| { type: 'move'; x: number; y: number }
| { type: 'drop' }
| { type: 'resolved' }
| { type: 'cancel' }
function dragReducer(state: DragState, action: DragAction): DragState {
switch (action.type) {
case 'start':
return { kind: 'dragging', startX: action.x, startY: action.y, offsetX: 0, offsetY: 0, pointerId: action.pointerId }
case 'move':
if (state.kind !== 'dragging') return state
return { ...state, offsetX: action.x - state.startX, offsetY: action.y - state.startY }
case 'drop':
if (state.kind !== 'dragging') return state
return { kind: 'dropping', offsetX: state.offsetX, offsetY: state.offsetY }
case 'resolved':
case 'cancel':
return { kind: 'idle' }
}
}
type DragHandlers = {
onPointerDown: (e: React.PointerEvent) => void
onPointerMove: (e: React.PointerEvent) => void
onPointerUp: (e: React.PointerEvent) => void
onPointerCancel: (e: React.PointerEvent) => void
}
export function useDrag(): {
state: DragState
handlers: DragHandlers
ref: React.RefObject<HTMLElement | null>
resolve: () => void
} {
const [state, dispatch] = useReducer(dragR
… (gekürzt für die Übersetzung)