mcpField
F
framer-motion

framer-motion

github.com/schoepplakeデザイン・UI5156 回インストール32 週間前に更新
リポジトリ

framer-motion について

React と Next.js の開発者向けに、Motion(Framer Motion)のアニメーション実装を解説するガイドです。一般的なパターン、パフォーマンス上の注意点、アクセシビリティを扱います。

主な機能

  1. 01スクロール起点と段階的なアニメーション
  2. 02レイアウトと退出アニメーション
  3. 03ジェスチャー、ドラッグ、スプリングアニメーション
  4. 04MotionValues によるフレーム更新
  5. 05視差効果の低減設定への対応

活用シーン

  1. 01ページ要素にスクロール時の表示効果を追加
  2. 02条件付きコンテンツの削除をアニメーション化
  3. 03ドラッグやジェスチャー操作のコンポーネントを構築
  4. 04スクロール進捗バーやカウンターを作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/schoepplake/framer-motion-skill --skill framer-motion

インストール後、Claude Code で/framer-motionを入力して読み込まれたか確認します。

詳細

Motion(Framer Motion)— アニメーションスキル

React と Next.js 向けの本番環境に対応したアニメーションパターンです。このスキルでは、Motion ライブラリ(v12 以降)を使って、正確でパフォーマンスに優れ、アクセシブルなアニメーションを記述する方法を説明します。

インポート

Motion は framer-motion から motion にブランド名を変更しました。どちらのパッケージ名も使用できますが、インポートパスが重要です。

// Client Components (standard React)
import { motion, AnimatePresence } from "motion/react"

// Next.js Server Components — use the client export
import * as motion from "motion/react-client"

// Legacy import (still works with the framer-motion package)
import { motion } from "framer-motion"

プロジェクトですでに framer-motion が依存関係として使われている場合は、一貫性を保つため、引き続き "framer-motion" からインポートしてください。インポート元を混在させないでください。

基本概念

motion.* コンポーネント

すべての HTML/SVG 要素には、対応する motion コンポーネントがあります。これらのコンポーネントはアニメーション用の props を受け取ります。

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
/>

重要: motion.* コンポーネントはクライアントコンポーネントです。Next.js App Router では、ファイルに "use client" を指定するか、クライアントコンポーネントでラップしてください。

MotionValues — 再レンダリングなしのアニメーション

useMotionValue は、React の再レンダリングを発生させずに更新される値を作成します。これは 60fps のアニメーションを実現する鍵です。

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])

return <motion.div style={{ x, opacity }} drag="x" />

ルール:

  • フレームごとに変化する値(スクロール位置、ドラッグ位置、連続アニメーション)には useMotionValue を使います。
  • 他の MotionValues から値を導出するには useTransform を使います(再レンダリングは発生しません)。
  • フレームごとの更新に useState を使わないでください。毎フレーム再レンダリングが発生します。
トランジション

アニメーションが状態間をどのように移動するかを制御します。

// Spring (default for physical properties like x, y, scale)
transition={{ type: "spring", stiffness: 300, damping: 30 }}

// Tween (default for non-physical properties like opacity, color)
transition={{ type: "tween", duration: 0.3, ease: "easeInOut" }}

// Custom cubic bezier
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}

よく使うパターン

スクロールをトリガーにしたフェードイン

最もよく使われるアニメーションパターンです。whileInView を使い、IntersectionObserver を手動で使わないでください。

<motion.div
  initial={{ opacity: 0, y: 24 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, amount: 0.1 }}
  transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
  {children}
</motion.div>
  • viewport.once: true — 初回表示時にのみアニメーションします(マーケティングページで標準的です)。
  • viewport.amount — 要素のうち、表示される必要がある割合です(0.1 = 10%)。
  • viewport.margin — トリガー領域を広げます(例:"0px 0px 50px 0px")。
子要素を順番に表示する

variants を使って、子要素を1つずつアニメーションさせます。

const container = {
  hidden: {},
  show: { transition: { staggerChildren: 0.1 } },
}

const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
}

<motion.ul variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
  {items.map((i) => (
    <motion.li key={i} variants={item}>{i}</motion.li>
  ))}
</motion.ul>

Variants は伝播するため、親の状態変化は自動的に子要素へ反映されます。

AnimatePresence による終了アニメーション

条件付きでレンダリングされる要素をラップすると、削除される前に終了アニメーションを実行できます。

<AnimatePresence>
  {isOpen && (
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.95 }}
      transition={{ duration: 0.2 }}
    />
  )}
</AnimatePresence>

ルール:

  • 子要素には一意の key が必要です。
  • exit prop で終了アニメーションを定義します。
  • 次の要素が入る前に終了アニメーションを完了させるには、mode="wait" を使います。
  • レイアウトを考慮したトランジションには mode="popLayout" を使います。
スクロールに連動したアニメーション

アニメーションをスクロール位置に直接連動させます。

const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.8])

return <motion.div style={{ opacity, scale }} />

要素ごとにスクロールをトラッキングするには、次のようにします。

const ref = useRef(null)
const { scrollYProgress } = useScroll({
  target: ref,
  offset: ["start end", "end start"], // when element enters/exits viewport
})
滑らかなスプリング値

MotionValues のトランジションを非常に滑らかにするには、useSpring を使います。

const scrollY = useMotionValue(0)
const smoothY = useSpring(scrollY, { stiffness: 100, damping: 30 })
連続アニメーション(useAnimationFrame)

グラデーションのきらめきやカウントなど、毎フレーム実行されるアニメーションには、次のようにします。

const progress = useMotionValue(0)

useAnimationFrame((time, delta) => {
  const newValue = (time / 1000) % 100
  progress.set(newValue)
})

const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)

return <motion.span style={{ backgroundPosition }} />
レイアウトアニメーション

レイアウトの変更を自動的にアニメーションさせます。

// Simple layout animation
<motion.div layout />

// Shared layout animation between components
<motion.div layoutId="hero-image" />

異なる2つのコンポーネントに同じ layoutId がある場合、Motion はそれらの間のアニメーションを行います(例:サムネイルから全画面表示への切り替え)。

ジェスチャーアニメーション
<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.97 }}
  whileFocus={{ outline: "2px solid #5227FF" }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>
ドラッグ
<motion.div
  drag           // enable both axes
  drag="x"       // constrain to x-axis
  dragConstraints={{ left: -100, right: 100 }}
  dragElastic={0.2}
  onDragEnd={(e, info) => {
    if (info.offset.x > 100) handleSwipeRight()
  }}
/>

アクセシビリティ

useReducedMotion

ユーザーのモーション設定を必ず尊重してください。これは任意ではなく、アクセシビリティ上の要件です。

import { useReducedMotion } from "framer-motion"

function AnimatedComponent({ children }) {
  const prefersReducedMotion = useReducedMotion()

  if (prefersReducedMotion) {
    return <div>{children}</div>
  }

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
    >
      {children}
    </motion.div>
  )
}

パターン:useReducedMotion() を確認し、アニメーションを完全に省略するか、移動を伴わない不透明度のみの変化に抑えます。

パフォーマンスのルール

  1. width、height、top、left はアニメーションさせないでください。 レイアウトの再計算が発生します。代わりに transform プロパティ(x、y、scale、rotate)を使います。

  2. フレームごとの更新には MotionValues を使います。 useState を使うと毎フレーム再レンダリングが発生します。MotionValues は DOM を直接更新します。

  3. will-change: transform は Motion コンポーネントによって自動的に追加されます。 手動で追加しないでください。

  4. **layout アニメーションは負荷が高いため、**すべての要素に使わず、必要な箇所でのみ使ってください。

  5. box-shadow のアニメーションは避けてください。 代わりに filter: drop-shadow() を使うか、疑似要素のシャドウの不透明度をアニメーションさせてください。

  6. opacity と transform を優先してください。 これらは GPU で合成され、別スレッドで実行されます。

  7. インタラクティブな要素(ホバー、タップ、ドラッグ)には、トゥイーン/イージングよりスプリングトランジションのほうが自然です。 トゥイーンはスクロールをトリガーにした表示時のアニメーションに使います。

よくある間違い

間違い修正方法
ドラッグ位置に useState を使うuseMotionValue を使います
IntersectionObserver を手動で使うwhileInView プロパティを使います
スタガーに setTimeout を使うstaggerChildren を指定した variants を使います
AnimatePresence に key がない子要素に一意の key を追加します
Server Components でアニメーションする"use client" を追加するか、motion/react-client を使います
モーションの軽減を無視する常に useReducedMotion() を確認します
initial を指定せず、マウント時に animate する開始状態を定義するために initial を設定します

レシピ

スクロールに応じたプログレスバー
const { scrollYProgress } = useScroll()
return (
  <motion.div
    className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left z-50"
    style={{ scaleX: scrollYProgress }}
  />
)
アニメーション付きカウンター
const count = useMotionValue(0)
const rounded = useTransform(count, (v) => Math.round(v))

useEffect(() => {
  const controls = animate(count, target, { duration: 2 })
  return controls.stop
}, [target])

return <motion.span>{rounded}</motion.span>
ページ遷移(Next.js App Router)
// template.tsx
"use client"
import { motion } from "framer-motion"

export default function Template({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  )
}
アニメーション付きグラデーションテキスト(シマー効果)
const progress = useMotionValue(0)

useAnimationFrame((time) => {
  const duration = 8000
  const fullCycle = duration * 2
  const cycleTime = (time % fullCycle)
  if (cycleTime < duration) {
    progress.set((cycleTime / duration) * 100)
  } else {
    progress.set(100 - ((cycleTime - duration) / duration) * 100)
  }
})

const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)

return (
  <motion.span
    className="bg-clip-text text-transparent"
    style={{
      backgroundImage: "linear-gradient(to right, #5227FF, #a78bfa, #c084fc, #5227FF)",
      backgroundSize: "300% 100%",
      backgroundPosition,
    }}
  >
    {text}
  </motion.span>
)

コメント