T-CREATOR

業務アプリも華やかに!実務で使える React アニメーション事例 10 選

業務アプリも華やかに!実務で使える React アニメーション事例 10 選

業務アプリケーション開発において、アニメーションは単なる装飾ではありません。ユーザーの操作を直感的に理解させ、システムの状態を分かりやすく伝える重要な要素です。

従来の業務アプリは機能性を重視するあまり、ユーザー体験が置き去りにされてきました。しかし、現代のユーザーは日常的に美しいアニメーションに触れているため、業務アプリでも自然な動きを期待するようになっています。

この記事では、実務で即座に活用できる React アニメーション事例を 10 選ご紹介します。難しい理論は最小限に抑え、実際のコードと共に理解を深めていただける構成にしました。

業務アプリでアニメーションが必要な理由

ユーザー体験の向上

業務アプリのユーザーは、一日中同じ画面に向き合っています。静的な画面だけでは、長時間の作業で疲労が蓄積し、操作ミスが増加する傾向があります。

アニメーションを適切に配置することで、ユーザーの注意力を自然に誘導し、作業効率を向上させることができます。特に、データの変化やシステムの状態変化を視覚的に表現することで、ユーザーは直感的に状況を把握できるようになります。

操作の視覚的フィードバック

ボタンをクリックした際に何も変化がないと、ユーザーは「操作が正しく実行されたのか」と不安になります。適切なアニメーションは、操作の成功や失敗を即座に伝える重要な役割を果たします。

例えば、フォーム送信時にローディングアニメーションを表示することで、ユーザーは処理中であることを理解し、待機時間に対するストレスを軽減できます。

データの変化を分かりやすく表現

業務アプリでは、大量のデータを扱うことが多いです。テーブルの行が追加・削除されたり、チャートの値が更新されたりする際に、アニメーションがあることで変化を分かりやすく表現できます。

静的な画面では、どのデータが変更されたのかを把握するのに時間がかかりますが、アニメーションによって瞬時に理解できるようになります。

React アニメーションライブラリの選定基準

パフォーマンス要件

業務アプリでは、大量のデータを扱うことが多いため、アニメーションライブラリのパフォーマンスが重要です。特に以下の点に注意が必要です。

  • レンダリング性能: 60fps での滑らかなアニメーション
  • メモリ使用量: 長時間使用してもメモリリークが発生しない
  • CPU 負荷: 他の処理に影響を与えない軽量な実装

学習コスト

開発チーム全体でアニメーションを実装する場合、学習コストの低いライブラリを選ぶことが重要です。

  • ドキュメントの充実度: 日本語ドキュメントやサンプルコードの豊富さ
  • コミュニティの活発さ: 質問やバグ報告への対応速度
  • API の直感性: 直感的に理解できる API 設計

メンテナンス性

長期的なプロジェクトでは、メンテナンス性が重要です。

  • 型安全性: TypeScript との相性
  • バージョンアップの頻度: 安定した API の提供
  • 後方互換性: 既存コードへの影響

実務で使える React アニメーション事例 10 選

1. ローディング状態の表現

データ取得中や処理実行中に、ユーザーに待機状態を伝えるローディングアニメーションは最も基本的で重要なアニメーションです。

まず、シンプルなスピナーコンポーネントを作成します。

typescript// LoadingSpinner.tsx
import React from 'react';
import styled, { keyframes } from 'styled-components';

const spin = keyframes`
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
`;

const Spinner = styled.div`
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: ${spin} 1s linear infinite;
`;

interface LoadingSpinnerProps {
  size?: number;
  color?: string;
}

const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
  size = 40,
  color = '#3498db',
}) => {
  return (
    <Spinner
      style={{
        width: size,
        height: size,
        borderTopColor: color,
      }}
    />
  );
};

export default LoadingSpinner;

次に、実際の使用例を示します。

typescript// DataTable.tsx
import React, { useState, useEffect } from 'react';
import LoadingSpinner from './LoadingSpinner';

interface DataTableProps {
  data: any[];
  loading: boolean;
}

const DataTable: React.FC<DataTableProps> = ({
  data,
  loading,
}) => {
  if (loading) {
    return (
      <div
        style={{
          display: 'flex',
          justifyContent: 'center',
          padding: '2rem',
        }}
      >
        <LoadingSpinner size={50} color='#2c3e50' />
      </div>
    );
  }

  return <table>{/* テーブルコンテンツ */}</table>;
};

このアニメーションにより、ユーザーは処理中であることを理解し、待機時間に対するストレスを軽減できます。

2. データテーブルの行追加・削除

テーブルの行が動的に追加・削除される際のアニメーションは、データの変化を視覚的に分かりやすく表現します。

React Transition Group を使用した実装例です。

typescript// AnimatedTable.tsx
import React, { useState } from 'react';
import {
  TransitionGroup,
  CSSTransition,
} from 'react-transition-group';
import styled from 'styled-components';

const TableRow = styled.tr`
  &.row-enter {
    opacity: 0;
    transform: translateX(-20px);
  }

  &.row-enter-active {
    opacity: 1;
    transform: translateX(0);
    transition: all 300ms ease-in;
  }

  &.row-exit {
    opacity: 1;
    transform: translateX(0);
  }

  &.row-exit-active {
    opacity: 0;
    transform: translateX(20px);
    transition: all 300ms ease-out;
  }
`;

interface TableData {
  id: number;
  name: string;
  value: number;
}

const AnimatedTable: React.FC = () => {
  const [data, setData] = useState<TableData[]>([
    { id: 1, name: '項目A', value: 100 },
    { id: 2, name: '項目B', value: 200 },
  ]);

  const addRow = () => {
    const newId = Math.max(...data.map((d) => d.id)) + 1;
    setData([
      ...data,
      {
        id: newId,
        name: `項目${String.fromCharCode(
          65 + data.length
        )}`,
        value: Math.floor(Math.random() * 1000),
      },
    ]);
  };

  const removeRow = (id: number) => {
    setData(data.filter((item) => item.id !== id));
  };

  return (
    <div>
      <button onClick={addRow}>行を追加</button>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>名前</th>
            <th></th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <TransitionGroup component={null}>
            {data.map((item) => (
              <CSSTransition
                key={item.id}
                timeout={300}
                classNames='row'
                component={TableRow}
              >
                <tr>
                  <td>{item.id}</td>
                  <td>{item.name}</td>
                  <td>{item.value}</td>
                  <td>
                    <button
                      onClick={() => removeRow(item.id)}
                    >
                      削除
                    </button>
                  </td>
                </tr>
              </CSSTransition>
            ))}
          </TransitionGroup>
        </tbody>
      </table>
    </div>
  );
};

このアニメーションにより、データの追加・削除が視覚的に分かりやすくなり、ユーザーは変化を瞬時に理解できます。

3. モーダルウィンドウの開閉

モーダルウィンドウの開閉アニメーションは、ユーザーの注意を自然に誘導し、コンテキストの切り替えを明確にします。

Framer Motion を使用した実装例です。

typescript// AnimatedModal.tsx
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styled from 'styled-components';

const Overlay = styled(motion.div)`
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
`;

const ModalContent = styled(motion.div)`
  background: white;
  padding: 2rem;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
`;

interface AnimatedModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

const AnimatedModal: React.FC<AnimatedModalProps> = ({
  isOpen,
  onClose,
  children,
}) => {
  return (
    <AnimatePresence>
      {isOpen && (
        <Overlay
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onClick={onClose}
        >
          <ModalContent
            initial={{
              opacity: 0,
              scale: 0.8,
              y: 50,
            }}
            animate={{
              opacity: 1,
              scale: 1,
              y: 0,
            }}
            exit={{
              opacity: 0,
              scale: 0.8,
              y: 50,
            }}
            onClick={(e) => e.stopPropagation()}
          >
            {children}
          </ModalContent>
        </Overlay>
      )}
    </AnimatePresence>
  );
};

使用例を示します。

typescript// ModalExample.tsx
import React, { useState } from 'react';
import AnimatedModal from './AnimatedModal';

const ModalExample: React.FC = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>
        モーダルを開く
      </button>

      <AnimatedModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
      >
        <h2>モーダルタイトル</h2>
        <p>モーダルの内容がここに表示されます。</p>
        <button onClick={() => setIsModalOpen(false)}>
          閉じる
        </button>
      </AnimatedModal>
    </div>
  );
};

このアニメーションにより、モーダルの開閉が自然で滑らかになり、ユーザーの注意を効果的に集めることができます。

4. フォーム送信の成功・エラー表示

フォーム送信後の結果表示は、ユーザーに操作の結果を明確に伝える重要な要素です。

typescript// FormNotification.tsx
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styled from 'styled-components';

const Notification = styled(motion.div)`
  padding: 1rem;
  border-radius: 4px;
  margin: 1rem 0;
  font-weight: 500;
`;

const SuccessNotification = styled(Notification)`
  background: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
`;

const ErrorNotification = styled(Notification)`
  background: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
`;

interface FormNotificationProps {
  type: 'success' | 'error';
  message: string;
  isVisible: boolean;
}

const FormNotification: React.FC<FormNotificationProps> = ({
  type,
  message,
  isVisible,
}) => {
  const NotificationComponent =
    type === 'success'
      ? SuccessNotification
      : ErrorNotification;

  return (
    <AnimatePresence>
      {isVisible && (
        <NotificationComponent
          initial={{
            opacity: 0,
            y: -20,
            scale: 0.95,
          }}
          animate={{
            opacity: 1,
            y: 0,
            scale: 1,
          }}
          exit={{
            opacity: 0,
            y: -20,
            scale: 0.95,
          }}
          transition={{
            type: 'spring',
            stiffness: 300,
            damping: 30,
          }}
        >
          {message}
        </NotificationComponent>
      )}
    </AnimatePresence>
  );
};

実際のフォームでの使用例です。

typescript// ContactForm.tsx
import React, { useState } from 'react';
import FormNotification from './FormNotification';

const ContactForm: React.FC = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
  });
  const [notification, setNotification] = useState<{
    type: 'success' | 'error';
    message: string;
    isVisible: boolean;
  }>({ type: 'success', message: '', isVisible: false });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      // 実際のAPI呼び出しをシミュレート
      await new Promise((resolve) =>
        setTimeout(resolve, 1000)
      );

      setNotification({
        type: 'success',
        message:
          'お問い合わせを送信しました。ありがとうございます。',
        isVisible: true,
      });

      setFormData({ name: '', email: '' });
    } catch (error) {
      setNotification({
        type: 'error',
        message:
          '送信に失敗しました。もう一度お試しください。',
        isVisible: true,
      });
    }

    // 3秒後に通知を非表示
    setTimeout(() => {
      setNotification((prev) => ({
        ...prev,
        isVisible: false,
      }));
    }, 3000);
  };

  return (
    <form onSubmit={handleSubmit}>
      <FormNotification
        type={notification.type}
        message={notification.message}
        isVisible={notification.isVisible}
      />

      <div>
        <label>お名前:</label>
        <input
          type='text'
          value={formData.name}
          onChange={(e) =>
            setFormData({
              ...formData,
              name: e.target.value,
            })
          }
          required
        />
      </div>

      <div>
        <label>メールアドレス:</label>
        <input
          type='email'
          value={formData.email}
          onChange={(e) =>
            setFormData({
              ...formData,
              email: e.target.value,
            })
          }
          required
        />
      </div>

      <button type='submit'>送信</button>
    </form>
  );
};

このアニメーションにより、ユーザーは操作の結果を即座に理解でき、適切な次のアクションを取ることができます。

5. ナビゲーションメニューの展開

サイドバーやドロップダウンメニューの展開アニメーションは、UI の階層構造を分かりやすく表現します。

typescript// AnimatedSidebar.tsx
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styled from 'styled-components';

const SidebarContainer = styled(motion.div)`
  position: fixed;
  left: 0;
  top: 0;
  height: 100vh;
  background: #2c3e50;
  color: white;
  overflow: hidden;
`;

const MenuItem = styled(motion.div)`
  padding: 1rem 2rem;
  cursor: pointer;
  border-bottom: 1px solid #34495e;

  &:hover {
    background: #34495e;
  }
`;

const ToggleButton = styled.button`
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 1001;
  padding: 0.5rem 1rem;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
`;

interface AnimatedSidebarProps {
  isOpen: boolean;
  onToggle: () => void;
}

const AnimatedSidebar: React.FC<AnimatedSidebarProps> = ({
  isOpen,
  onToggle,
}) => {
  const menuItems = [
    { id: 1, label: 'ダッシュボード', icon: '📊' },
    { id: 2, label: 'ユーザー管理', icon: '👥' },
    { id: 3, label: '設定', icon: '⚙️' },
    { id: 4, label: 'レポート', icon: '📈' },
  ];

  return (
    <>
      <ToggleButton onClick={onToggle}>
        {isOpen ? '閉じる' : '開く'}
      </ToggleButton>

      <AnimatePresence>
        {isOpen && (
          <SidebarContainer
            initial={{ x: -300 }}
            animate={{ x: 0 }}
            exit={{ x: -300 }}
            transition={{
              type: 'spring',
              stiffness: 300,
              damping: 30,
            }}
          >
            {menuItems.map((item, index) => (
              <MenuItem
                key={item.id}
                initial={{ x: -50, opacity: 0 }}
                animate={{ x: 0, opacity: 1 }}
                transition={{
                  delay: index * 0.1,
                  type: 'spring',
                  stiffness: 300,
                }}
              >
                <span style={{ marginRight: '1rem' }}>
                  {item.icon}
                </span>
                {item.label}
              </MenuItem>
            ))}
          </SidebarContainer>
        )}
      </AnimatePresence>
    </>
  );
};

このアニメーションにより、メニューの展開が自然で滑らかになり、ユーザーは直感的にナビゲーションを操作できます。

6. チャート・グラフのデータ更新

データビジュアライゼーションにおけるアニメーションは、数値の変化を視覚的に分かりやすく表現します。

typescript// AnimatedChart.tsx
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import styled from 'styled-components';

const ChartContainer = styled.div`
  width: 100%;
  height: 300px;
  background: #f8f9fa;
  border-radius: 8px;
  padding: 1rem;
  position: relative;
`;

const Bar = styled(motion.div)`
  background: linear-gradient(
    135deg,
    #667eea 0%,
    #764ba2 100%
  );
  border-radius: 4px 4px 0 0;
  position: absolute;
  bottom: 0;
  width: 40px;
  margin: 0 10px;
`;

const BarLabel = styled.div`
  position: absolute;
  bottom: -30px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  color: #666;
`;

const ValueLabel = styled.div`
  position: absolute;
  top: -25px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  font-weight: bold;
  color: #333;
`;

interface ChartData {
  label: string;
  value: number;
}

const AnimatedChart: React.FC = () => {
  const [data, setData] = useState<ChartData[]>([
    { label: '1月', value: 30 },
    { label: '2月', value: 45 },
    { label: '3月', value: 60 },
    { label: '4月', value: 35 },
    { label: '5月', value: 80 },
  ]);

  const maxValue = Math.max(...data.map((d) => d.value));

  const updateData = () => {
    setData((prevData) =>
      prevData.map((item) => ({
        ...item,
        value: Math.floor(Math.random() * 100) + 10,
      }))
    );
  };

  return (
    <div>
      <button onClick={updateData}>データを更新</button>
      <ChartContainer>
        {data.map((item, index) => (
          <div
            key={item.label}
            style={{
              position: 'absolute',
              left: `${
                (index / (data.length - 1)) * 80 + 10
              }%`,
              height: '100%',
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
            }}
          >
            <Bar
              style={{
                height: `${
                  (item.value / maxValue) * 200
                }px`,
              }}
              initial={{ height: 0 }}
              animate={{
                height: `${
                  (item.value / maxValue) * 200
                }px`,
              }}
              transition={{
                duration: 0.8,
                ease: 'easeOut',
              }}
            />
            <ValueLabel>{item.value}</ValueLabel>
            <BarLabel>{item.label}</BarLabel>
          </div>
        ))}
      </ChartContainer>
    </div>
  );
};

このアニメーションにより、データの変化が視覚的に分かりやすくなり、トレンドやパターンを瞬時に理解できます。

7. 通知・アラートの表示

システムからの通知やアラートは、ユーザーに重要な情報を伝える重要な要素です。

typescript// NotificationSystem.tsx
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styled from 'styled-components';

const NotificationContainer = styled.div`
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
`;

const NotificationItem = styled(motion.div)`
  background: white;
  border-radius: 8px;
  padding: 1rem 1.5rem;
  margin-bottom: 10px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  border-left: 4px solid;
  min-width: 300px;
`;

const SuccessNotification = styled(NotificationItem)`
  border-left-color: #27ae60;
`;

const ErrorNotification = styled(NotificationItem)`
  border-left-color: #e74c3c;
`;

const WarningNotification = styled(NotificationItem)`
  border-left-color: #f39c12;
`;

const InfoNotification = styled(NotificationItem)`
  border-left-color: #3498db;
`;

interface Notification {
  id: string;
  type: 'success' | 'error' | 'warning' | 'info';
  title: string;
  message: string;
  duration?: number;
}

const NotificationSystem: React.FC = () => {
  const [notifications, setNotifications] = useState<
    Notification[]
  >([]);

  const addNotification = (
    notification: Omit<Notification, 'id'>
  ) => {
    const id = Date.now().toString();
    const newNotification = { ...notification, id };

    setNotifications((prev) => [...prev, newNotification]);

    // 自動削除
    setTimeout(() => {
      removeNotification(id);
    }, notification.duration || 5000);
  };

  const removeNotification = (id: string) => {
    setNotifications((prev) =>
      prev.filter((n) => n.id !== id)
    );
  };

  const getNotificationComponent = (
    notification: Notification
  ) => {
    const baseProps = {
      initial: { x: 300, opacity: 0 },
      animate: { x: 0, opacity: 1 },
      exit: { x: 300, opacity: 0 },
      transition: {
        type: 'spring',
        stiffness: 300,
        damping: 30,
      },
    };

    switch (notification.type) {
      case 'success':
        return <SuccessNotification {...baseProps} />;
      case 'error':
        return <ErrorNotification {...baseProps} />;
      case 'warning':
        return <WarningNotification {...baseProps} />;
      case 'info':
        return <InfoNotification {...baseProps} />;
      default:
        return <NotificationItem {...baseProps} />;
    }
  };

  return (
    <div>
      <div style={{ marginBottom: '2rem' }}>
        <button
          onClick={() =>
            addNotification({
              type: 'success',
              title: '成功',
              message: '操作が正常に完了しました。',
            })
          }
        >
          成功通知
        </button>

        <button
          onClick={() =>
            addNotification({
              type: 'error',
              title: 'エラー',
              message:
                '操作に失敗しました。もう一度お試しください。',
            })
          }
        >
          エラー通知
        </button>

        <button
          onClick={() =>
            addNotification({
              type: 'warning',
              title: '警告',
              message: '注意が必要な操作です。',
            })
          }
        >
          警告通知
        </button>

        <button
          onClick={() =>
            addNotification({
              type: 'info',
              title: '情報',
              message: '新しい機能が利用可能になりました。',
            })
          }
        >
          情報通知
        </button>
      </div>

      <NotificationContainer>
        <AnimatePresence>
          {notifications.map((notification) => (
            <div key={notification.id}>
              {React.cloneElement(
                getNotificationComponent(notification),
                {
                  onClick: () =>
                    removeNotification(notification.id),
                }
              )}
              <h4>{notification.title}</h4>
              <p>{notification.message}</p>
            </div>
          ))}
        </AnimatePresence>
      </NotificationContainer>
    </div>
  );
};

このアニメーションにより、通知が自然に表示され、ユーザーの注意を効果的に集めることができます。

8. ページ遷移のトランジション

SPA におけるページ遷移アニメーションは、ユーザーに現在の位置を明確に伝えます。

typescript// PageTransition.tsx
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useLocation } from 'react-router-dom';

const PageContainer = styled(motion.div)`
  min-height: 100vh;
  padding: 2rem;
`;

const pageVariants = {
  initial: {
    opacity: 0,
    x: -20,
    scale: 0.98,
  },
  in: {
    opacity: 1,
    x: 0,
    scale: 1,
  },
  out: {
    opacity: 0,
    x: 20,
    scale: 0.98,
  },
};

const pageTransition = {
  type: 'tween',
  ease: 'anticipate',
  duration: 0.4,
};

interface PageTransitionProps {
  children: React.ReactNode;
}

const PageTransition: React.FC<PageTransitionProps> = ({
  children,
}) => {
  const location = useLocation();

  return (
    <AnimatePresence mode='wait'>
      <PageContainer
        key={location.pathname}
        initial='initial'
        animate='in'
        exit='out'
        variants={pageVariants}
        transition={pageTransition}
      >
        {children}
      </PageContainer>
    </AnimatePresence>
  );
};

このアニメーションにより、ページ遷移が自然で滑らかになり、ユーザーは現在の位置を明確に理解できます。

9. ドラッグ&ドロップ操作

ドラッグ&ドロップ操作のアニメーションは、ユーザーの操作を視覚的に分かりやすく表現します。

typescript// DraggableList.tsx
import React, { useState } from 'react';
import { motion, Reorder } from 'framer-motion';
import styled from 'styled-components';

const ListItem = styled(motion.div)`
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 0.5rem;
  cursor: grab;
  user-select: none;

  &:active {
    cursor: grabbing;
  }

  &.dragging {
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
    transform: rotate(5deg);
  }
`;

const DraggableList: React.FC = () => {
  const [items, setItems] = useState([
    { id: 1, text: 'タスク 1', priority: '高' },
    { id: 2, text: 'タスク 2', priority: '中' },
    { id: 3, text: 'タスク 3', priority: '低' },
    { id: 4, text: 'タスク 4', priority: '高' },
    { id: 5, text: 'タスク 5', priority: '中' },
  ]);

  return (
    <div>
      <h3>優先度順に並び替えてください</h3>
      <Reorder.Group
        axis='y'
        values={items}
        onReorder={setItems}
      >
        {items.map((item) => (
          <Reorder.Item
            key={item.id}
            value={item}
            whileDrag={{
              scale: 1.05,
              boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)',
            }}
            transition={{
              type: 'spring',
              stiffness: 300,
              damping: 30,
            }}
          >
            <ListItem>
              <div
                style={{
                  display: 'flex',
                  justifyContent: 'space-between',
                }}
              >
                <span>{item.text}</span>
                <span
                  style={{
                    color:
                      item.priority === '高'
                        ? '#e74c3c'
                        : item.priority === '中'
                        ? '#f39c12'
                        : '#27ae60',
                  }}
                >
                  {item.priority}
                </span>
              </div>
            </ListItem>
          </Reorder.Item>
        ))}
      </Reorder.Group>
    </div>
  );
};

このアニメーションにより、ドラッグ&ドロップ操作が直感的で分かりやすくなり、ユーザーは自然に操作を実行できます。

10. プログレスバーの進行状況

長時間の処理や段階的な操作における進行状況の表示は、ユーザーの不安を軽減します。

typescript// AnimatedProgressBar.tsx
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import styled from 'styled-components';

const ProgressContainer = styled.div`
  width: 100%;
  background: #f0f0f0;
  border-radius: 10px;
  overflow: hidden;
  margin: 1rem 0;
`;

const ProgressBar = styled(motion.div)`
  height: 20px;
  background: linear-gradient(
    90deg,
    #667eea 0%,
    #764ba2 100%
  );
  border-radius: 10px;
  position: relative;
`;

const ProgressText = styled.div`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: white;
  font-weight: bold;
  font-size: 12px;
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
`;

const StepIndicator = styled.div`
  display: flex;
  justify-content: space-between;
  margin-top: 1rem;
`;

const Step = styled.div<{
  active: boolean;
  completed: boolean;
}>`
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: bold;
  background: ${(props) =>
    props.completed
      ? '#27ae60'
      : props.active
      ? '#3498db'
      : '#bdc3c7'};
  color: white;
  transition: all 0.3s ease;
`;

interface AnimatedProgressBarProps {
  currentStep: number;
  totalSteps: number;
  onComplete?: () => void;
}

const AnimatedProgressBar: React.FC<
  AnimatedProgressBarProps
> = ({ currentStep, totalSteps, onComplete }) => {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const newProgress = (currentStep / totalSteps) * 100;
    setProgress(newProgress);

    if (currentStep === totalSteps && onComplete) {
      setTimeout(onComplete, 500);
    }
  }, [currentStep, totalSteps, onComplete]);

  const steps = Array.from(
    { length: totalSteps },
    (_, i) => i + 1
  );

  return (
    <div>
      <ProgressContainer>
        <ProgressBar
          initial={{ width: 0 }}
          animate={{ width: `${progress}%` }}
          transition={{
            duration: 0.8,
            ease: 'easeOut',
          }}
        >
          <ProgressText>
            {Math.round(progress)}%
          </ProgressText>
        </ProgressBar>
      </ProgressContainer>

      <StepIndicator>
        {steps.map((step) => (
          <Step
            key={step}
            active={step === currentStep}
            completed={step < currentStep}
          >
            {step < currentStep ? '✓' : step}
          </Step>
        ))}
      </StepIndicator>
    </div>
  );
};

使用例を示します。

typescript// ProgressExample.tsx
import React, { useState } from 'react';
import AnimatedProgressBar from './AnimatedProgressBar';

const ProgressExample: React.FC = () => {
  const [currentStep, setCurrentStep] = useState(1);
  const totalSteps = 5;

  const handleNext = () => {
    if (currentStep < totalSteps) {
      setCurrentStep((prev) => prev + 1);
    }
  };

  const handleReset = () => {
    setCurrentStep(1);
  };

  const handleComplete = () => {
    alert('すべてのステップが完了しました!');
  };

  return (
    <div>
      <h3>
        ステップ {currentStep} / {totalSteps}
      </h3>

      <AnimatedProgressBar
        currentStep={currentStep}
        totalSteps={totalSteps}
        onComplete={handleComplete}
      />

      <div style={{ marginTop: '2rem' }}>
        <button
          onClick={handleNext}
          disabled={currentStep >= totalSteps}
        >
          次のステップ
        </button>

        <button
          onClick={handleReset}
          style={{ marginLeft: '1rem' }}
        >
          リセット
        </button>
      </div>
    </div>
  );
};

このアニメーションにより、ユーザーは進行状況を明確に理解でき、長時間の処理に対する不安を軽減できます。

実装時の注意点

パフォーマンス最適化

アニメーションは美しいですが、パフォーマンスに影響を与える可能性があります。以下の点に注意してください。

GPU アクセラレーションの活用

css/* パフォーマンスを向上させる CSS プロパティ */
.animated-element {
  transform: translateZ(
    0
  ); /* GPU アクセラレーションを強制 */
  will-change: transform; /* ブラウザに変更を事前通知 */
}

アニメーションの最適化

typescript// 不要なアニメーションを避ける
const shouldAnimate = (element: HTMLElement) => {
  const rect = element.getBoundingClientRect();
  const isVisible =
    rect.top < window.innerHeight && rect.bottom > 0;
  return isVisible;
};

アクセシビリティ配慮

アニメーションはすべてのユーザーにとって快適とは限りません。アクセシビリティを考慮した実装が重要です。

prefers-reduced-motion の対応

typescript// ユーザーの設定を尊重
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

const animationConfig = prefersReducedMotion
  ? {
      duration: 0,
      ease: 'linear',
    }
  : {
      duration: 0.3,
      ease: 'easeOut',
    };

フォーカス管理

typescript// キーボードナビゲーションのサポート
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    handleAction();
  }
};

ブラウザ互換性

現代的なアニメーションライブラリは多くのブラウザをサポートしていますが、古いブラウザでの動作を考慮する必要があります。

フォールバックの実装

typescript// ブラウザの機能をチェック
const supportsWebAnimations =
  'animate' in Element.prototype;

if (supportsWebAnimations) {
  // モダンなアニメーション
  element.animate(keyframes, options);
} else {
  // フォールバック
  element.style.transition = 'all 0.3s ease';
  element.style.transform = 'translateX(100px)';
}

まとめ

業務アプリケーションにおけるアニメーションは、単なる装飾ではなく、ユーザー体験を向上させる重要な要素です。

今回ご紹介した 10 の事例は、実務で即座に活用できる実用的なアニメーションです。それぞれのアニメーションは、ユーザーの操作を直感的に理解させ、システムの状態を分かりやすく伝える役割を果たします。

重要なのは、アニメーションを適切に配置し、ユーザーの作業効率を向上させることです。過度なアニメーションは逆にユーザーを混乱させる可能性があるため、控えめで効果的な実装を心がけてください。

また、パフォーマンスやアクセシビリティを考慮した実装により、すべてのユーザーにとって快適なアプリケーションを作成できます。

これらの事例を参考に、あなたの業務アプリケーションにも適切なアニメーションを導入し、ユーザー体験を向上させてください。小さな改善が、大きな満足度の向上につながることを実感していただけるはずです。

関連リンク