Communitygithub.com

KilianBStar/motion-patterns

Produktionsreife Animationsmuster für React / Next.js – Button, Modal, Toast, Stagger, Seitenübergänge, Exit-Animationen, Scroll und Layout – basierend auf motion-foundations-Tokens und Federn. Zum Animieren eines bestimmten UI-Elements in React oder Next.js: Button, Modal, Toast, Stagger, Seitenübergang oder Scroll.

Was ist motion-patterns?

Diese Skill bietet eine Sammlung von Copy-Paste-fähigen Animationsmustern für React und Next.js, die auf den Tokens und Federn (Springs) von `motion-foundations` aufbauen. Sie deckt die häufigsten UI-Interaktionen ab: Button-Feedback, Modal-Dialoge mit vollständiger Barrierefreiheit, gestaffelte Listeneinblendungen (Stagger), Seitenübergänge im Next.js App Router, Scroll-Reveal-Effekte, akkordionartige Erweiterungen und Shared-Element-Übergänge. Alle Muster importieren vordefinierte Werte aus `motion-foundations`, sodass keine neuen Dauer- oder Easing-Werte definiert werden müssen. Die Skill betont die korrekte Verwendung von `AnimatePresence` mit `key` und `exit`, um Exit-Animationen zuverlässig auszulösen. Sie enthält klare Regeln für Abstände, Layout-Animationen und Barrierefreiheit sowie Anti-Patterns zur Vermeidung typischer Fehler. Die Plattformen sind React 19+, Next.js App Router und jedes Framework, das `motion/react` unterstützt. Entwickler können die Muster direkt in ihre Projekte übernehmen, ohne sie anpassen zu müssen.

Funktioniert mit~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/KilianBStar/mes-skills-/tree/HEAD/.claude/skills/motion-patterns

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Bewegungsmuster (Motion Patterns)

Copy-Paste-Muster für die häufigsten UI-Animationen. Jedes Muster hier basiert auf den motion-foundations-Tokens und Federn. Definieren Sie hier keine neuen Dauern oder Easing-Werte – importieren Sie sie.

Wann aktivieren

  • Animieren eines Buttons, einer Karte, eines Modals oder einer Toast-Benachrichtigung
  • Erstellen von Listeneingängen mit Staffelung (Stagger)
  • Einrichten von Seitenübergängen im Next.js App Router
  • Hinzufügen von Ein- und Ausgangsanimationen zu bedingten Inhalten
  • Implementieren von Scroll-Reveal, scrollgebundenem Fortschritt oder klebrigen Story-Abschnitten
  • Erstellen von erweiterbaren Karten, Akkordeons oder Shared-Element-Übergängen

Ausgaben

Diese Skill produziert:

  • Barrierefreie, SSR-sichere Animationen für alle Standard-UI-Komponenten
  • AnimatePresence-umwickelte bedingte Render mit korrektem Ausgangsverhalten
  • Seitenübergangs-Wrapper-Komponente für den Next.js App Router
  • Scroll-Reveal und scrollgebundene Muster mit useScroll + useTransform
  • Layout-Animationsmuster (layout, layoutId) zum Erweitern und Überblenden von Elementen

Prinzipien

  • Jedes Muster importiert aus motion-foundations. Keine rohen Zahlen.
  • Jeder bedingte Render wird in AnimatePresence mit einem key umwickelt.
  • Ausgangsanimationen werden immer zusammen mit Eingangsanimationen definiert – niemals als nachträglicher Einfall.
  • layout wird nur für kleine, isolierte Verschiebungen verwendet. Große Teilbäume erhalten explizite Transformationen.

Regeln

  1. Umwickeln Sie bedingte Render immer mit AnimatePresence und einem key auf dem direkten Kind. Ohne Schlüssel werden Ausgangsanimationen niemals ausgelöst.
  2. Definieren Sie immer exit, wenn Sie initial + animate definieren. Eine Animation ohne Ausgang ist unvollständig.
  3. Verwenden Sie mode="wait" bei Seitenübergängen. Der Eingang darf nicht beginnen, bevor der Ausgang abgeschlossen ist.
  4. Verwenden Sie layout niemals in Teilbäumen mit mehr als ~5 Kindern oder tief verschachteltem DOM. Verwenden Sie stattdessen explizite x/y-Transformationen.
  5. Das Staffelungsintervall muss zwischen 0,05s und 0,10s liegen. Darunter wirkt es mechanisch; darüber träge.
  6. Modals müssen immer enthalten: Fokusfalle, Escape-Taste zum Schließen, Scroll-Sperre, role="dialog", aria-modal="true".
  7. Scroll-Reveals verwenden viewport={{ once: true }}. Wiederholungen beim Verlassen des Bildschirms lenken ab, informieren nicht.
  8. Alle Token-Werte werden aus motion-foundations importiert. Keine Inline-Zahlen.

Entscheidungshilfe

Das richtige Muster wählen

SituationMuster
Element erscheint/verschwindetAnimatePresence
Liste von Elementen laden nacheinanderStagger variants
Navigation zwischen RoutenSeitenübergangs-Wrapper
Element ändert die Größe an Ort und Stellelayout prop
Gleiches Element bewegt sich über SeitenkontextelayoutId
Element tritt ein, wenn es in den Viewport gescrollt wirdwhileInView
Wert an die Scrollposition gebundenuseScroll + useTransform

Wann mode="wait" vs. mode="sync" verwenden

ModusVerwenden, wenn
waitSeitenübergänge, Inhaltswechsel (einer nach dem anderen)
syncGestapelte Benachrichtigungen, Listenelemente (Überlappung ist in Ordnung)
popLayoutElemente aus einer umfließenden Liste entfernt

Kernkonzepte

AnimatePresence-Vertrag

Drei Dinge müssen immer wahr sein:

  1. AnimatePresence umwickelt den Bedingungsausdruck
  2. Das direkte Kind hat einen key
  3. Das Kind hat eine exit-Eigenschaft

Fehlt eines davon, schlägt die Ausgangsanimation still.

layout vs. layoutId

  • layout – animiert die eigene Größen-/Positionsänderung des Elements an Ort und Stelle
  • layoutId – verbindet zwei separate Elemente und überblendet sie zwischen den Rendervorgängen

Verwenden Sie layout="position" für Text in einem expandierenden Container, um zu verhindern, dass Text-Umfließen animiert wird.

Code-Beispiele

Button-Feedback

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.button
  whileHover={{ scale: motionTokens.scale.pop }}
  whileTap={{ scale: motionTokens.scale.press }}
  transition={springs.snappy}
/>

Staffelliste (Stagger)

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

const container = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.08,   // innerhalb der 0,05–0,10 Regel
      delayChildren: 0.1,
    },
  },
}

const item = {
  hidden:  { opacity: 0, y: motionTokens.distance.md },
  visible: { opacity: 1, y: 0, transition: springs.gentle },
}

<motion.ul variants={container} initial="hidden" animate="visible">
  {items.map((i) => (
    <motion.li key={i.id} variants={item} />
  ))}
</motion.ul>

Modal

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

// Am Aufrufort umwickeln:
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>

export function Modal({ onClose }: { onClose: () => void }) {
  return (
    <>
      {/* Overlay */}
      <motion.div
        className="fixed inset-0 bg-black/50"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        onClick={onClose}
      />

      {/* Panel — Barrierefreiheitsanforderungen: Fokusfalle, Escape schließen,
          Scroll-Sperre, role="dialog", aria-modal="true" */}
      <motion.div
        role="dialog"
        aria-modal="true"
        className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
        initial={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        animate={{ opacity: 1, scale: 1, y: 0 }}
        exit={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        transition={springs.gentle}
      />
    </>
  )
}

Toast-Stapel

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<AnimatePresence mode="sync">
  {toasts.map((t) => (
    <motion.div
      key={t.id}
      layout
      initial={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      animate={{ opacity: 1, x: 0, scale: 1 }}
      exit={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      transition={springs.snappy}
    />
  ))}
</AnimatePresence>

Seitenübergang (Next.js App Router)

// components/page-transition.tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { usePathname } from "next/navigation"
import { motionTokens } from "@/lib/motion-tokens"

const variants = {
  initial: { opacity: 0, y: motionTokens.distance.sm },
  enter:   { opacity: 1, y: 0 },
  exit:    { opacity: 0, y: -motionTokens.distance.sm },
}

export function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        variants={variants}
        initial="initial"
        animate="enter"
        exit="exit"
        transition={{
          duration: motionTokens.duration.normal,
          ease: motionTokens.easing.smooth,
        }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

Scroll-Reveal

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<motion.div
  initial={{ opacity: 0, y: motionTokens.distance.lg }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: "-80px" }}   // once: true — Regel 7
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

Scroll-Fortschrittsbalken

"use client"
import { motion, useScroll } from "motion/react"

export function ScrollProgress() {
  const { scrollYProgress } = useScroll()
  return (
    <motion.div
      className="fixed top-0 left-0 h-1 bg-indigo-500 origin-left w-full"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

Erweiterbare Karte

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

export function ExpandingCard({ title, body }: { title: string; body: string }) {
  const [expanded, setExpanded] = useState(false)

  return (
    <motion.div layout onClick={() => setExpanded(!expanded)} className="cursor-pointer">
      {/* layout="position" verhindert, dass Text-Umfließen animiert wird */}
      <motion.h2 layout="position" className="font-semibold">
        {title}
      </motion.h2>

      <AnimatePresence>
        {expanded && (
          <motion.p
            key="body"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {body}
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  )
}

Shared-Element-Überblendung

// Quellkontext
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />

// Zielkontext (gleiches layoutId — motion übernimmt den Übergang)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />

Akkordeon

<motion.div
  initial={false}
  animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
  style={{ transformOrigin: "top", overflow: "hidden" }}
  transition={{
    duration: motionTokens.duration.normal,
    ease: motionTokens.easing.smooth,
  }}
> {children}
</motion.div>

End-to-End-Beispiel

Eine staffellierte Liste, die beim Mounten erscheint, bedingte Präsenz handhabt und reduzierte Bewegung respektiert – und dabei Tokens, Federn, AnimatePresence und den Barrierefreiheits-Hook aus motion-foundations kombiniert:

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"

const containerVariants = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08, delayChildren: 0.1 },
  },
}

function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
  const safe = useSafeMotion(motionTokens.distance.sm)
  return (
    <motion.li
      variants={{
        hidden:  safe.initial,
        visible: safe.animate,
      }}
      exit={safe.exit}
      transition={springs.gentle}
      className="flex items-center justify-between p-3 rounded-lg bg-white shadow-sm"
    >
      <span>{label}</span>
      <button onClick={onRemove}>Entfernen</button>
    </motion.li>
  )
}

export function AnimatedList({ items, onRemove }: {
  items: { id: string; label: string }[]
  onRemove: (id: string) => void
}) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-2"
    >
      <AnimatePresence mode="popLayout">
        {items.map((item) => (
          <ListItem
            key={item.id}
            label={item.label}
            onRemove={() => onRemove(item.id)}
          />
        ))}
      </AnimatePresence>
    </motion.ul>
  )
}

Einschränkungen / Nicht-Ziele

Diese Skill deckt nicht ab:

  • Token- und Federdefinitionen → siehe motion-foundations
  • Zieh-Interaktionen, Wischgesten, neu anordenbare Listen → siehe motion-advanced
  • Textanimationen (Wort-/Zeichen-Enthüllungen, Zähler) → siehe motion-advanced
  • SVG-Pfadzeichnen oder Morphing → siehe motion-advanced
  • Benutzerdefinierte Animations-Hooks → siehe motion-advanced
  • Nur-CSS-Übergänge ohne motion/react

Anti-Patterns

Anti-PatternVerstoßene RegelBehebung
AnimatePresence-Kind ohne keyRegel 1Stabiles key zum direkten Kind hinzufügen
initial + animate ohne exitRegel 2Alle drei immer zusammen definieren
Seitenübergang ohne mode="wait"Regel 3mode="wait" zu AnimatePresence hinzufügen
layout auf einer 50-Elemente-ListeRegel 4mode="popLayout" oder explizite Transformationen verwenden
staggerChildren: 0.2 auf einer 10-Elemente-ListeRegel 5Auf 0,08–0,10 begrenzen
Modal ohne FokusfalleRegel 6focus-trap-react oder Radix Dialog hinzufügen
whileInView ohne viewport={{ once: true }}Regel 7Wiederholte Eingänge lenken ab, informieren nicht
transition={{ duration: 0.3 }} inlineRegel 8motionTokens.duration.normal verwenden

Verwandte Skills

  • motion-foundations — definiert alle Tokens, Federn, den useSafeMotion-Hook und SSR-Sicherungen, die jedes Muster hier importiert. Muss zuerst eingerichtet werden.
  • motion-advanced — erweitert diese Muster um Ziehen, Gesten, SVG, Text, benutzerdefinierte Hooks und imperative Sequenzierung. Definiert keine Muster aus dieser Skill neu.

Verwandte Skills