404 Not Found

404 Not Found


nginx

トランジションとアニメーション

トムはデータダッシュボードのプロジェクトを終えたばかりだったが、プロダクトマネージャーから、ページの遷移が唐突すぎると指摘された。リストが読み込まれる際にデータが「ちらつき」ながら表示され、ポップアップも瞬時に開いたり閉じたりしていたのだ。トムは、すべての機能を盛り込んだからといって、必ずしもユーザー体験が良好になるとは限らないことに気づいた。彼は、ページに滑らかな遷移アニメーションを追加する必要があった。


1. 学習内容


2. 概念図

以下の図は、アニメーションのオプションを選択する際に、素早く判断を下すのに役立ちます:

100%
flowchart TD
    A[Animation effects are needed] --> B{Effect Complexity}
    B -->|Simple Transition| C[CSS Transition]
    B -->|Keyframe Animation| D[CSS Animation]
    B -->|Complex Interactions| E[Framer Motion]

    C --> C1["hover / focus Effect"]
    C --> C2["opacity / transform Fade Animation"]
    C --> C3["Changes to a Single Attribute"]

    D --> D1["@keyframes Defining Multi-Stage"]
    D --> D2["Infinite Loop Animation"]
    D --> D3["Loading the skeleton screen"]

    E --> E1["mount/unmount Animation"]
    E --> E2["Drag / Gestures"]
    E --> E3["List layout Animation"]
    E --> E4["Spring Physical Animation Effects"]

    style C fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
    style E fill:#fff3e0,stroke:#e65100

3. 実際の事例

トムのダッシュボードには、タスクリストの読み込みモーダルウィンドウの表示・非表示の切り替えカードのドラッグと並べ替えという3つの主要な操作が含まれています。彼の目標は:

  1. タスクリストが読み込まれると、各タスクが左側からスライドして表示されます。
  2. ポップアップが開くと、背景がフェードインし、ポップアップがズームインします。閉じる際は、このアニメーションが逆再生されます。
  3. カードをドラッグすると指の動きに合わせて移動し、指を離すと元の位置に戻ります。

彼は最もシンプルなCSSトランジションから始め、徐々にFramer Motionを使って複雑なアニメーションを作成するようになった。


(1) CSSトランジション — 最もシンプルなプログレッシブ・エンハンスメント

CSSのトランジションは、初期状態最終状態の間で滑らかな遷移を実現するために使用されます。JavaScriptライブラリは一切必要とせず、CSSで宣言するだけで効果を発揮します。

遷移の4つの主要な部分特性

CSS
/* transition: property duration timing-function delay */
.example {
  transition: opacity 0.3s ease 0s;
  /* Complete Syntax:
     transition-property: opacity
     transition-duration: 0.3s
     transition-timing-function: ease
     transition-delay: 0s
  */
}

timing-function の一般的な値:

(1) ▶ サンプル:CSS トランジションを使用したアコーディオンコンポーネントの実装

JSX
import { useState } from 'react'

const accordionData = [
  { title: 'What is React?', content: 'React It is a tool for building user interfaces. JavaScript library.' },
  { title: 'What Is a Component??', content: 'A component is React The smallest independent unit of an application,Reusable and combinable。' },
  { title: 'What is State?', content: 'State It is variable data within the component,Driver UI Update。' },
]

function Accordion() {
  const [openIndex, setOpenIndex] = useState(null)

  function toggle(index) {
    setOpenIndex(openIndex === index ? null : index)
  }

  return (
    <div style={{ maxWidth: 500, fontFamily: 'sans-serif' }}>
      {accordionData.map((item, index) => (
        <div
          key={index}
          style={{
            border: '1px solid #e8e8e8',
            borderRadius: 8,
            marginBottom: 8,
            overflow: 'hidden',
          }}
        >
          <button
            onClick={() => toggle(index)}
            style={{
              width: '100%',
              padding: '12px 16px',
              background: openIndex === index ? '#e6f7ff' : '#fafafa',
              border: 'none',
              cursor: 'pointer',
              fontSize: 15,
              fontWeight: 600,
              textAlign: 'left',
              transition: 'background 0.2s ease',
            }}
          >
            {item.title}
          </button>
          <div
            style={{
              maxHeight: openIndex === index ? 80 : 0,
              padding: openIndex === index ? '12px 16px' : '0 16px',
              opacity: openIndex === index ? 1 : 0,
              transition: 'all 0.3s ease',
              color: '#666',
              fontSize: 14,
              lineHeight: 1.6,
            }}
          >
            {item.content}
          </div>
        </div>
      ))}
    </div>
  )
}
▶ 試してみよう

ポイント: maxHeight + opacity + padding を同時に遷移させることで、コンテンツ領域の展開・折りたたみ効果を実現します。なお、maxHeight の値は、実際のコンテンツの高さよりわずかに大きく設定する必要があります。


(2) CSSアニメーション — キーフレーム駆動型アニメーション

アニメーションの段階が 2 つ以上ある場合(単に「開始→終了」を超える場合)や、ループさせる必要がある場合は、CSS アニメーションの方が適しています。CSS アニメーションでは、@keyframes を使用して、複数の段階からなるアニメーションシーケンスを定義します。

主要な属性

CSS
/* animation: name duration timing-function delay iteration-count direction fill-mode */
.pulse {
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
  0%   { transform: scale(1); opacity: 1; }
  50%  { transform: scale(1.05); opacity: 0.7; }
  100% { transform: scale(1); opacity: 1; }
}

(2) ▶ サンプル:スケルトン画面の読み込みアニメーション

JSX
import './Skeleton.css'

function Skeleton({ count = 3 }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      {Array.from({ length: count }).map((_, i) => (
        <div key={i} className="skeleton-row">
          <div className="skeleton-avatar" />
          <div style={{ flex: 1 }}>
            <div className="skeleton-line" style={{ width: '60%' }} />
            <div className="skeleton-line" style={{ width: '90%', marginTop: 8 }} />
          </div>
        </div>
      ))}
    </div>
  )
}

export default Skeleton
▶ 試してみよう
CSS
/* Skeleton.css */
.skeleton-row {
  display: flex;
  gap: 12px;
  align-items: center;
}

.skeleton-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
}

.skeleton-line {
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
}

@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

スケルトン画面のアニメーションの仕組み: background-position の動きは、読み込み中に画面を横切る光の軌跡という視覚効果を再現しています。CSSアニメーションの infinite プロパティにより、実際のコンテンツの読み込みが完了してスケルトン画面が置き換わるまで、この光の軌跡が繰り返し再生されます。


(3) Framer Motion — 複雑な React アニメーション

Framer Motionは、Reactエコシステムにおいて最も強力なアニメーションライブラリであり、以下の3つの主要な機能を提供しています:

機能 解決される課題 対応するAPI
表示・非表示のアニメーション コンポーネントのマウント・アンマウント時のアニメーション motion.div + AnimatePresence
レイアウトアニメーション リストの順序が変わったときのスムーズな遷移 layout プロパティ
ジェスチャーアニメーション ドラッグ、ホバー、タップのジェスチャー drag / whileHover / whileTap

インストール

BASH
npm install framer-motion

(3) ▶ サンプル:Framer Motion を使った To-Do リストの実装

JSX
import { useState } from 'react'
import { motion, AnimatePresence, Reorder } from 'framer-motion'

const initialTodos = [
  { id: 1, text: 'Done React Learning Animation', done: false },
  { id: 2, text: 'Reworking the Dashboard Components', done: false },
  { id: 3, text: 'Code Review PR #142', done: false },
  { id: 4, text: 'Update Project Documentation', done: true },
  { id: 5, text: 'Fix the Responsive Navigation BarBUG', done: false },
]

function TodoList() {
  const [todos, setTodos] = useState(initialTodos)

  function toggleDone(id) {
    setTodos(prev =>
      prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
    )
  }

  function removeTodo(id) {
    setTodos(prev => prev.filter(t => t.id !== id))
  }

  return (
    <div style={{ maxWidth: 480, fontFamily: 'sans-serif' }}>
      <AnimatePresence>
        {todos.map(todo => (
          <motion.div
            key={todo.id}
            layout
            initial={{ opacity: 0, x: -60 }}
            animate={{
              opacity: 1,
              x: 0,
              background: todo.done ? '#f6ffed' : '#fff',
            }}
            exit={{ opacity: 0, x: 100, height: 0, marginBottom: 0 }}
            transition={{ type: 'spring', stiffness: 300, damping: 25 }}
            style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'space-between',
              padding: '12px 16px',
              marginBottom: 8,
              borderRadius: 8,
              border: '1px solid #f0f0f0',
              cursor: 'pointer',
            }}
            whileHover={{ scale: 1.02 }}
            whileTap={{ scale: 0.98 }}
          >
            <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => toggleDone(todo.id)}
                style={{ width: 18, height: 18, cursor: 'pointer' }}
              />
              <span
                style={{
                  textDecoration: todo.done ? 'line-through' : 'none',
                  color: todo.done ? '#999' : '#333',
                  fontSize: 15,
                }}
              >
                {todo.text}
              </span>
            </div>
            <button
              onClick={() => removeTodo(todo.id)}
              style={{
                border: 'none',
                background: 'none',
                color: '#ff4d4f',
                cursor: 'pointer',
                fontSize: 18,
                fontWeight: 'bold',
              }}
            >
              x
            </button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}

export default TodoList
▶ 試してみよう

このコードは、Framer Motionの4つの主な機能を紹介しています:

  1. initial / animate / exit:コンポーネントのエントリ、表示、およびエグジットの状態を定義します。AnimatePresence を使用すると、条件付きレンダリングによってエグジットアニメーションもトリガーできます。
  2. layout プロパティdone の状態変化によりリスト項目の位置が変更された場合、座標を計算することなく、自動的に新しい位置へ滑らかに遷移します。
  3. transitionspringの物理スプリングアニメーションを使用します。stiffnessはスプリングの剛性を制御し、dampingは減衰を制御します(値が大きいほど、弾力性が弱くなります)。
  4. whileHover / whileTap:インタラクティブなジェスチャーアニメーション。マウスをホバーさせると1.02倍に拡大し、クリックすると0.98倍に縮小します。

(4) ▶ サンプル:Framer Motion によるモーダルポップアップアニメーション

JSX
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'

function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          key="overlay"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.2 }}
          style={{
            position: 'fixed',
            inset: 0,
            background: 'rgba(0,0,0,0.45)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            zIndex: 1000,
          }}
          onClick={onClose}
        >
          <motion.div
            key="modal"
            initial={{ scale: 0.85, opacity: 0, y: 20 }}
            animate={{ scale: 1, opacity: 1, y: 0 }}
            exit={{ scale: 0.85, opacity: 0, y: 20 }}
            transition={{ type: 'spring', stiffness: 400, damping: 30 }}
            style={{
              background: '#fff',
              padding: 24,
              borderRadius: 12,
              minWidth: 320,
              boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
              position: 'relative',
            }}
            onClick={e => e.stopPropagation()}
          >
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

function App() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button
        onClick={() => setIsOpen(true)}
        style={{ padding: '8px 20px', fontSize: 15, cursor: 'pointer' }}
      >
        Open the pop-up window
      </button>

      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
        <h2 style={{ margin: '0 0 12px' }}>Confirm Action</h2>
        <p style={{ color: '#666', marginBottom: 20 }}>
          Are you sure you want to delete this record??This action cannot be undone.。
        </p>
        <div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
          <button
            onClick={() => setIsOpen(false)}
            style={{ padding: '8px 20px', cursor: 'pointer' }}
          >
            Cancel
          </button>
          <button
            onClick={() => {
              alert('Deleted')
              setIsOpen(false)
            }}
            style={{
              padding: '8px 20px',
              background: '#ff4d4f',
              color: '#fff',
              border: 'none',
              borderRadius: 6,
              cursor: 'pointer',
            }}
          >
            Confirm Deletion
          </button>
        </div>
      </Modal>
    </div>
  )
}
▶ 試してみよう

ポップアップアニメーションを設計する際の要点:


4. アニメーションのパフォーマンスに関するベストプラクティス

属性 トリガーフェーズ パフォーマンス 推奨事項
transform 複合 ✅ 最高 ⭐⭐⭐
opacity 複合 ✅ 最高 ⭐⭐⭐
filter ペイント ⚠️ 中級 ⭐⭐
box-shadow ペイント ⚠️ 中級
width/height レイアウト ❌ 高価
top/left/margin レイアウト ❌ 高価
ルール 説明
デフォルトでは transform および opacity を使用 これらのプロパティは、レイアウトやペイントを引き起こすことなくコンポジタースレッドを起動します
アニメーションを避ける width / height / top / left これらのプロパティはレイアウトのリフローを引き起こし、リソースを大量に消費します
will-change を使用してブラウザにプロンプトを表示する will-change: transform, opacity を使用して事前に合成レイヤーを作成する
一度にアニメーションさせる要素の数を多くしすぎない 20個以上の要素を同時にアニメーションさせると、フレーム落ちが発生する可能性があります
requestAnimationFrame の使用 JavaScript によるアニメーションで setTimeout の使用を避ける

(1) ▶ サンプル:CSS キーフレームアニメーション — 読み込み中のスケルトン画面

JSX
function SkeletonCard() {
  return (
    <div style={{ border: '1px solid #f0f0f0', borderRadius: 8, padding: 16, overflow: 'hidden' }}>
      <div style={{
        height: 120, borderRadius: 4, marginBottom: 12,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      <div style={{
        height: 16, width: '60%', borderRadius: 4, marginBottom: 8,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      <div style={{
        height: 12, width: '40%', borderRadius: 4,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      <style>{`
        @keyframes shimmer {
          0% { background-position: 200% 0; }
          100% { background-position: -200% 0; }
        }
      `}</style>
    </div>
  )
}

function LoadingGrid() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, maxWidth: 800, margin: '0 auto' }}>
      {[1, 2, 3].map(i => <SkeletonCard key={i} />)}
    </div>
  )
}
▶ 試してみよう

❓ よくある質問

Q CSSのトランジションとアニメーションの違いは具体的に何ですか?
A トランジションとは、状態の変化(ホバーなど)によってトリガーされる「AからBへ」という2段階の遷移であり、ループはサポートされていません。一方、アニメーションは多段階のキーフレームアニメーションであり、無限にループさせたり、逆再生したり、一時停止や再開を行うことができます。単純なシナリオにはトランジションを、複雑なシーケンスにはアニメーションを使用してください。
Q Framer Motion の AnimatePresence が時々機能しないのはなぜですか?
A 最も一般的な原因は、key が正しく設定されていないことです。AnimatePresencekeyを介して子要素を追跡します。キーが変更されていない場合、その要素を「削除された」ではなく「更新された」と解釈してしまうため、退出アニメーションがトリガーされません。また、AnimatePresenceの直接の子要素は、motionコンポーネントでなければなりません。
Q モバイル端末でアニメーションのパフォーマンスを確保するにはどうすればよいですか?
A transform および opacity アニメーションのみを使用するようにしてください。これらのプロパティは、モバイルの GPU 合成レイヤー上で実行されるためです。Framer Motion はデフォルトで transform を使用しており、これにより良好なパフォーマンスが確保されます。モバイルデバイスで複雑なアニメーションを作成する必要がある場合は、useWillChangeフックを使用するか、will-changeを手動で設定してください。
Q アニメーションによってレイアウトがずれてしまった場合はどうすればよいですか?
A アニメーション中のレイアウトの変化を防ぐため、アニメーション対象の要素には固定サイズを確保してください。Framer Motionでは、layoutプロパティをlayoutIdおよびAnimatePresenceと組み合わせて使用することで、安定したレイアウト遷移を実現できます。CSSを使用する場合は、位置アニメーションにおいてleftの代わりにtransform: translateX()の使用を検討してください。
Q React 18のuseTransitionとFramer Motionの違いは何ですか?
A useTransitionはReactに組み込まれた並行処理機能で、特定の状態更新(検索結果のフィルタリングなど)を「非緊急」としてマークし、緊急の更新(入力フィールドへの入力など)を優先的に処理することで、カクつきを防ぐものです。Framer Motionは、UI要素の移動、拡大縮小、不透明度などの視覚効果を扱うアニメーションライブラリです。この2つは異なる課題に対処しています。useTransitionは「インタラクションの遅延」に対処するのに対し、Framer Motionは「視覚的な遷移」に対処します。

📖 まとめ


📝 練習問題

  1. CSSのトランジションを使用して「ページトップへ」ボタンを実装する:ページが300px以上スクロールされるとフェードインし、クリックするとスムーズにページトップまでスクロールし、ページトップに到達するとフェードアウトして非表示になる。
  2. CSSアニメーションを使用して、ローディングスピナー(回転する円のアニメーション)を作成します。@keyframes を使用して回転角度を 0deg から 360deg まで制御し、linear を使用して速度曲線を設定し、一定の速度を維持します。
  3. Framer Motion を使用して、ドラッグ可能なフローティングアクションボタン(FAB)を作成します。drag プロパティを設定することで、画面上の任意の場所にドラッグできるようになります。ドラッグが終了したら、spring アニメーションを使用して、最も近い画面の端にスナップさせます。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%