Reactでアニメーションを実装|CSS Transitionの基本

ReactでCSS Transitionを使ったアニメーション実装方法を詳しく解説。フェードイン・フェードアウト、スライドアニメーション、ホバー効果の作り方から、パフォーマンスの最適化まで初心者向けに説明します。

Learning Next 運営
38 分で読めます

みなさん、Reactでアニメーションを作りたいと思ったことはありませんか?

「ボタンをクリックしたら、ふわっと要素が現れるようにしたい」 「ページが切り替わる時に、スムーズなアニメーションを付けたい」

こんな風に思ったことありませんか? でも、アニメーションって難しそうで、なかなか手を出せずにいるかもしれませんね。

大丈夫です! 実は、CSS Transitionを使えば、思っているより簡単にアニメーションが作れるんです。

この記事では、Reactで使えるCSS Transitionの基本から実践的な使い方まで、初心者の方にもわかりやすく解説します。 最後まで読めば、あなたもスムーズで美しいアニメーションを作れるようになりますよ!

CSS Transitionって何?

CSS Transitionは、CSSのプロパティが変化する時に、その変化を滑らかにアニメーション化してくれる機能です。

簡単に言うと、「急にパッと変わる」のではなく、「ゆっくりと変化する」ようにしてくれるものなんです。

基本的な仕組みを理解しよう

CSS Transitionは、こんな4つの要素で構成されています。

.element {
  /* どのプロパティをアニメーション化するか */
  transition-property: opacity, transform;
  
  /* アニメーションの時間 */
  transition-duration: 0.3s;
  
  /* アニメーションの動き方 */
  transition-timing-function: ease-in-out;
  
  /* 遅延時間 */
  transition-delay: 0s;
  
  /* まとめて書くこともできます */
  transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
}

見てください! 上記のコードでは、opacity(透明度)とtransform(変形)というプロパティを、0.3秒かけて滑らかに変化させるという設定をしています。

ease-in-outは、最初はゆっくり、途中で速くなって、最後にまたゆっくりになる動きです。 自然な感じの動きになるので、よく使われます。

Reactでの基本的な使い方

実際にReactで使ってみましょう!

import { useState } from 'react';
import './FadeAnimation.css';

function FadeAnimation() {
  const [isVisible, setIsVisible] = useState(false);
  
  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? '非表示' : '表示'}
      </button>
      
      <div 
        className={`fade-element ${isVisible ? 'visible' : 'hidden'}`}
      >
        アニメーション対象の要素
      </div>
    </div>
  );
}

JavaScriptの部分では、useStateを使ってisVisibleという状態を管理しています。 ボタンをクリックすると、この状態がtruefalseで切り替わります。

そして、その状態に応じて要素にvisiblehiddenのクラスが付きます。

次に、CSSの部分を見てみましょう。

/* FadeAnimation.css */
.fade-element {
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

.fade-element.visible {
  opacity: 1;
}

.fade-element.hidden {
  opacity: 0;
}

最初の.fade-elementで、基本的な透明度を0(完全に透明)に設定しています。 そして、transitionで透明度の変化を0.3秒かけて行うように指定しています。

.visibleクラスが付いた時は透明度が1(完全に不透明)になり、.hiddenクラスが付いた時は透明度が0になります。

この設定により、ボタンをクリックすると要素がふわっと現れたり消えたりするアニメーションが完成します!

フェードイン・フェードアウトを作ってみよう

基本がわかったところで、もう少し実践的なフェードアニメーションを作ってみましょう。

リストアイテムのフェードアニメーション

今度は、リストに項目を追加・削除する時のアニメーションを作ってみます。

function FadeInOut() {
  const [items, setItems] = useState([]);
  
  const addItem = () => {
    const newItem = {
      id: Date.now(),
      text: `アイテム ${items.length + 1}`,
      isVisible: false
    };
    
    setItems(prev => [...prev, newItem]);
    
    // 少し遅延してからアニメーション開始
    setTimeout(() => {
      setItems(prev => 
        prev.map(item => 
          item.id === newItem.id 
            ? { ...item, isVisible: true }
            : item
        )
      );
    }, 100);
  };
  
  const removeItem = (id) => {
    // まずアニメーションを開始
    setItems(prev => 
      prev.map(item => 
        item.id === id 
          ? { ...item, isVisible: false }
          : item
      )
    );
    
    // アニメーション完了後に要素を削除
    setTimeout(() => {
      setItems(prev => prev.filter(item => item.id !== id));
    }, 300);
  };
  
  return (
    <div>
      <button onClick={addItem}>アイテム追加</button>
      
      <div className="item-list">
        {items.map(item => (
          <div 
            key={item.id}
            className={`list-item ${item.isVisible ? 'fade-in' : 'fade-out'}`}
          >
            {item.text}
            <button onClick={() => removeItem(item.id)}>削除</button>
          </div>
        ))}
      </div>
    </div>
  );
}

ちょっと複雑に見えますが、順番に説明しますね。

まず、addItem関数では新しいアイテムを作成します。 最初はisVisible: falseで、見えない状態にしています。

その後、setTimeoutを使って100ミリ秒後にisVisible: trueに変更します。 これにより、アイテムが追加された瞬間にふわっと現れるアニメーションが実現できます。

removeItem関数では、まずisVisible: falseに設定してフェードアウトを開始します。 そして、300ミリ秒後(アニメーション完了後)に実際にアイテムを削除しています。

対応するCSSはこんな感じです。

.list-item {
  opacity: 0;
  transform: translateY(-20px);
  transition: opacity 0.3s ease-out, transform 0.3s ease-out;
  margin: 10px 0;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
}

.list-item.fade-in {
  opacity: 1;
  transform: translateY(0);
}

.list-item.fade-out {
  opacity: 0;
  transform: translateY(-20px);
}

ここでは、透明度だけでなくtransformも使っています。 translateY(-20px)は要素を上に20px移動させるという意味です。

つまり、アイテムが現れる時は「上から下に移動しながらフェードイン」し、消える時は「下から上に移動しながらフェードアウト」するアニメーションになります。

実際に動かしてみると、とても自然で美しいアニメーションが完成しますよ!

スライドアニメーションを実装しよう

次は、要素が横や縦にスライドするアニメーションを作ってみましょう。

横スライドアニメーション

よくあるスライダーのような動きを作ってみます。

function SlideAnimation() {
  const [currentIndex, setCurrentIndex] = useState(0);
  
  const slides = [
    { id: 1, title: 'スライド1', content: '最初のスライド' },
    { id: 2, title: 'スライド2', content: '2番目のスライド' },
    { id: 3, title: 'スライド3', content: '3番目のスライド' }
  ];
  
  const nextSlide = () => {
    setCurrentIndex((prev) => (prev + 1) % slides.length);
  };
  
  const prevSlide = () => {
    setCurrentIndex((prev) => (prev - 1 + slides.length) % slides.length);
  };
  
  return (
    <div className="slider-container">
      <div className="slider-wrapper">
        <div 
          className="slider-content"
          style={{ 
            transform: `translateX(-${currentIndex * 100}%)`,
            transition: 'transform 0.5s ease-in-out'
          }}
        >
          {slides.map(slide => (
            <div key={slide.id} className="slide">
              <h2>{slide.title}</h2>
              <p>{slide.content}</p>
            </div>
          ))}
        </div>
      </div>
      
      <div className="slider-controls">
        <button onClick={prevSlide}>前へ</button>
        <button onClick={nextSlide}>次へ</button>
      </div>
    </div>
  );
}

このコードの核心は、**transform: translateX(-${currentIndex * 100}%)**の部分です。

currentIndexが0の時はtranslateX(0%)で最初の位置、1の時はtranslateX(-100%)で左に一画面分移動、という具合に動きます。

スライドが3つあるので、インデックスが0、1、2と変わるたびに、画面全体が左右にスライドして見えるんです。

対応するCSSも見てみましょう。

.slider-container {
  width: 100%;
  max-width: 600px;
  margin: 0 auto;
}

.slider-wrapper {
  overflow: hidden;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.slider-content {
  display: flex;
  width: 300%; /* スライド数 × 100% */
}

.slide {
  flex: 0 0 100%;
  padding: 40px;
  text-align: center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.slider-controls {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 20px;
}

.slider-controls button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  background: #667eea;
  color: white;
  cursor: pointer;
  transition: background 0.2s ease;
}

.slider-controls button:hover {
  background: #5a6fd8;
}

ポイントは、.slider-contentwidth: 300%です。 スライドが3つあるので、全体の幅を300%にして、3つのスライドを横に並べています。

.slider-wrapperoverflow: hiddenを設定しているので、現在見えているスライド以外は隠れます。

縦スライドアニメーション

今度は、縦方向のスライドアニメーションを作ってみましょう。 よくあるアコーディオンのような動きです。

function VerticalSlide() {
  const [isExpanded, setIsExpanded] = useState(false);
  
  return (
    <div className="expandable-card">
      <div className="card-header" onClick={() => setIsExpanded(!isExpanded)}>
        <h3>詳細情報</h3>
        <span className={`arrow ${isExpanded ? 'expanded' : ''}`}>▼</span>
      </div>
      
      <div className={`card-content ${isExpanded ? 'expanded' : 'collapsed'}`}>
        <p>これは展開可能なコンテンツです。</p>
        <p>クリックすることで表示・非表示が切り替わります。</p>
        <p>スムーズなアニメーションで展開されます。</p>
      </div>
    </div>
  );
}

このコードでは、ヘッダーをクリックするとisExpandedの状態が切り替わります。 その状態に応じて、コンテンツ部分が展開されたり折り畳まれたりします。

対応するCSSを見てみましょう。

.expandable-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  margin: 20px 0;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background: #f8f9fa;
  cursor: pointer;
  transition: background 0.2s ease;
}

.card-header:hover {
  background: #e9ecef;
}

.arrow {
  transition: transform 0.3s ease;
}

.arrow.expanded {
  transform: rotate(180deg);
}

.card-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease-out, padding 0.3s ease-out;
}

.card-content.expanded {
  max-height: 200px;
  padding: 15px;
}

.card-content.collapsed {
  max-height: 0;
  padding: 0 15px;
}

ここでのポイントは、max-heightを使ってアニメーションを実現していることです。

通常、height: autoからheight: 0のようなアニメーションは上手く動きません。 でも、max-heightを使うことで、自然な高さの変化をアニメーション化できるんです。

.arrowの回転アニメーションも素敵ですよね。 transform: rotate(180deg)で矢印が180度回転して、開閉の状態を視覚的に表現しています。

ホバー効果でユーザー体験を向上させよう

マウスを要素の上に乗せた時(ホバー時)のアニメーションも、ユーザー体験を向上させる重要な要素です。

インタラクティブなボタンを作ろう

まずは、ホバー時に動きのあるボタンを作ってみましょう。

function AnimatedButton({ children, onClick }) {
  const [isHovered, setIsHovered] = useState(false);
  
  return (
    <button
      className={`animated-button ${isHovered ? 'hovered' : ''}`}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      onClick={onClick}
    >
      <span className="button-content">{children}</span>
      <span className="button-background"></span>
    </button>
  );
}

function ButtonDemo() {
  return (
    <div>
      <AnimatedButton onClick={() => alert('クリックされました')}>
        アニメーションボタン
      </AnimatedButton>
    </div>
  );
}

このボタンは、マウスが乗った時にonMouseEnterが発火し、離れた時にonMouseLeaveが発火します。 その状態に応じて、isHoveredの値が変わり、ボタンの見た目が変化します。

対応するCSSを見てみましょう。

.animated-button {
  position: relative;
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  background: #007bff;
  color: white;
  cursor: pointer;
  overflow: hidden;
  transition: all 0.3s ease;
}

.button-content {
  position: relative;
  z-index: 2;
  transition: transform 0.3s ease;
}

.button-background {
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
  transition: left 0.5s ease;
}

.animated-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}

.animated-button.hovered .button-background {
  left: 100%;
}

このCSSでは、複数のアニメーション効果を組み合わせています。

まず、:hoverの時にtranslateY(-2px)ボタンが少し上に浮き上がるような効果を付けています。 同時にbox-shadowで影を付けて、より立体的に見えるようにしています。

.button-backgroundは、ホバー時にボタンの上を光が流れるような効果を作っています。 最初はleft: -100%で画面外の左側にいて、ホバー時にleft: 100%で右側に移動します。

美しいカードアニメーション

次は、カードにホバー効果を付けてみましょう。

function AnimatedCard({ title, description, image }) {
  return (
    <div className="card-container">
      <div className="card-image">
        <img src={image} alt={title} />
        <div className="card-overlay">
          <span>詳細を見る</span>
        </div>
      </div>
      
      <div className="card-body">
        <h3>{title}</h3>
        <p>{description}</p>
      </div>
    </div>
  );
}

function CardGallery() {
  const cards = [
    {
      title: 'カード1',
      description: '説明文1',
      image: '/images/card1.jpg'
    },
    {
      title: 'カード2',
      description: '説明文2',
      image: '/images/card2.jpg'
    }
  ];
  
  return (
    <div className="card-gallery">
      {cards.map((card, index) => (
        <AnimatedCard key={index} {...card} />
      ))}
    </div>
  );
}

このカードは、シンプルなReactコンポーネントですが、CSSで素敵なアニメーション効果を付けています。

.card-container {
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  background: white;
}

.card-container:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}

.card-image {
  position: relative;
  overflow: hidden;
}

.card-image img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  transition: transform 0.3s ease;
}

.card-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.card-container:hover .card-image img {
  transform: scale(1.1);
}

.card-container:hover .card-overlay {
  opacity: 1;
}

.card-body {
  padding: 20px;
}

.card-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
  margin: 20px 0;
}

このカードでは、ホバー時に3つのアニメーション効果を組み合わせています。

  1. カード全体が上に浮き上がるtranslateY(-5px)
  2. 画像が少し拡大されるscale(1.1)
  3. オーバーレイが表示されるopacity: 0からopacity: 1

これらの効果が組み合わさることで、とても洗練された印象のカードアニメーションが完成します。

パフォーマンスを最適化しよう

アニメーションを作る時は、パフォーマンスのことも考える必要があります。

GPU加速を活用しよう

ブラウザには「GPU加速」という機能があります。 これを使うと、アニメーションがより滑らかに動作します。

.optimized-animation {
  /* transform と opacity のみを使用 */
  transform: translateZ(0); /* GPU加速を強制 */
  will-change: transform, opacity; /* ブラウザに最適化のヒントを提供 */
}

.slide-element {
  /* レイアウトを変更しないプロパティを使用 */
  transform: translateX(0);
  transition: transform 0.3s ease;
}

.slide-element.moved {
  transform: translateX(100px);
}

ここでのポイントは、transformopacityのみを使用していることです。

これらのプロパティは、ブラウザのレンダリングエンジンが最適化しやすく、滑らかなアニメーションを実現できます。

逆に、widthheightmarginなどのプロパティをアニメーション化すると、ブラウザがレイアウトを再計算する必要があり、重い処理になってしまいます。

不要な再レンダリングを防ごう

Reactでは、不要な再レンダリングもパフォーマンスに影響します。

// React.memo を使用してパフォーマンスを最適化
const AnimatedItem = React.memo(({ item, isVisible }) => {
  return (
    <div className={`item ${isVisible ? 'visible' : 'hidden'}`}>
      {item.content}
    </div>
  );
});

function OptimizedList() {
  const [items, setItems] = useState([]);
  const [visibleItems, setVisibleItems] = useState(new Set());
  
  // アニメーションの状態のみを更新
  const toggleVisibility = (id) => {
    setVisibleItems(prev => {
      const newSet = new Set(prev);
      if (newSet.has(id)) {
        newSet.delete(id);
      } else {
        newSet.add(id);
      }
      return newSet;
    });
  };
  
  return (
    <div>
      {items.map(item => (
        <AnimatedItem
          key={item.id}
          item={item}
          isVisible={visibleItems.has(item.id)}
        />
      ))}
    </div>
  );
}

React.memoを使うことで、propsが変わった時だけ再レンダリングされるようになります。

また、Setを使って表示状態を管理することで、効率的な状態更新を実現しています。

実践的なアニメーションパターン

実際の開発でよく使われるアニメーションパターンを見てみましょう。

ローディングアニメーション

データを取得中などに表示するローディングアニメーションです。

function LoadingSpinner() {
  return (
    <div className="loading-container">
      <div className="spinner"></div>
      <p>読み込み中...</p>
    </div>
  );
}

function DataLoader() {
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData()
      .then(result => {
        setData(result);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error:', error);
        setLoading(false);
      });
  }, []);
  
  if (loading) {
    return <LoadingSpinner />;
  }
  
  return (
    <div className="data-container fade-in">
      {data && (
        <div>
          <h2>データが読み込まれました</h2>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

このコードでは、データの取得中はLoadingSpinnerを表示し、取得完了後はデータを表示します。

対応するCSSはこんな感じです。

.loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 40px;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.data-container {
  opacity: 0;
  transition: opacity 0.5s ease-in;
}

.data-container.fade-in {
  opacity: 1;
}

スピナーは、@keyframesを使って無限に回転するアニメーションを作っています。 データが取得されると、.fade-inクラスが付いて、ふわっと表示されます。

通知メッセージのアニメーション

ユーザーにメッセージを表示する通知システムです。

function NotificationSystem() {
  const [notifications, setNotifications] = useState([]);
  
  const addNotification = (message, type = 'info') => {
    const id = Date.now();
    const notification = { id, message, type, visible: false };
    
    setNotifications(prev => [...prev, notification]);
    
    // アニメーション開始
    setTimeout(() => {
      setNotifications(prev =>
        prev.map(n => n.id === id ? { ...n, visible: true } : n)
      );
    }, 100);
    
    // 自動削除
    setTimeout(() => {
      removeNotification(id);
    }, 3000);
  };
  
  const removeNotification = (id) => {
    setNotifications(prev =>
      prev.map(n => n.id === id ? { ...n, visible: false } : n)
    );
    
    setTimeout(() => {
      setNotifications(prev => prev.filter(n => n.id !== id));
    }, 300);
  };
  
  return (
    <div>
      <div className="notification-buttons">
        <button onClick={() => addNotification('成功メッセージ', 'success')}>
          成功通知
        </button>
        <button onClick={() => addNotification('エラーメッセージ', 'error')}>
          エラー通知
        </button>
      </div>
      
      <div className="notification-container">
        {notifications.map(notification => (
          <div
            key={notification.id}
            className={`notification ${notification.type} ${
              notification.visible ? 'slide-in' : 'slide-out'
            }`}
          >
            {notification.message}
            <button onClick={() => removeNotification(notification.id)}>
              ×
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

この通知システムでは、以下のような流れでアニメーションが動きます。

  1. 通知が作成されると、まず見えない状態で配列に追加
  2. 100ミリ秒後にvisible: trueにしてスライドイン
  3. 3秒後にvisible: falseにしてスライドアウト
  4. 300ミリ秒後に配列から削除

対応するCSSです。

.notification-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
}

.notification {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  margin-bottom: 10px;
  border-radius: 6px;
  color: white;
  min-width: 300px;
  transform: translateX(100%);
  opacity: 0;
  transition: all 0.3s ease;
}

.notification.success {
  background: #28a745;
}

.notification.error {
  background: #dc3545;
}

.notification.info {
  background: #17a2b8;
}

.notification.slide-in {
  transform: translateX(0);
  opacity: 1;
}

.notification.slide-out {
  transform: translateX(100%);
  opacity: 0;
}

.notification button {
  background: none;
  border: none;
  color: white;
  cursor: pointer;
  font-size: 18px;
  padding: 0;
  margin-left: 10px;
}

通知は画面の右上から右にスライドインし、消える時は右にスライドアウトします。 position: fixedを使って、画面の固定位置に表示されます。

まとめ

お疲れ様でした! ReactでCSS Transitionを使ったアニメーションの実装方法を、たくさん学びましたね。

今回学んだ重要なポイントをまとめておきましょう。

CSS Transitionの基本

  • transitionプロパティで滑らかなアニメーションを実現
  • transformopacityを使うとパフォーマンスが良い
  • タイミング関数で自然な動きを演出

実装のコツ

  • 状態管理とCSSクラスの組み合わせが重要
  • setTimeoutを使ってアニメーションのタイミングを調整
  • React.memoで不要な再レンダリングを防ぐ

実践的なパターン

  • フェードイン・アウト、スライド、ホバー効果
  • ローディングアニメーション、通知システム
  • 複数のアニメーション効果の組み合わせ

アニメーションは、ユーザー体験を大きく向上させる重要な要素です。 最初は簡単なフェードアニメーションから始めて、徐々に複雑なものにチャレンジしてみてください。

何より大切なのは、実際に手を動かして試してみることです。 コードをコピーして、自分なりにアレンジしてみてくださいね!

きっと、あなたのアプリケーションがより魅力的になりますよ。 ぜひ、今日学んだ技術を実際のプロジェクトで活用してみてください!

関連記事