T-CREATOR

Emotion でアニメーション遅延・遷移を表現する

Emotion でアニメーション遅延・遷移を表現する

モダンな Web アプリケーションにおいて、滑らかで美しいアニメーションはユーザーエクスペリエンスを大きく向上させる重要な要素です。特に React アプリケーションでは、CSS-in-JS ライブラリを使ったアニメーション実装が主流となっています。

Emotion は、そんな CSS-in-JS ライブラリの中でも特にアニメーション機能が充実しており、遅延や遷移を使った魅力的なエフェクトを簡単に実装できます。本記事では、Emotion を使ってアニメーション遅延・遷移を表現する方法を、基礎から応用まで段階的に解説していきます。

背景

CSS-in-JS でのアニメーション実装の必要性

従来の Web 開発では、CSS ファイルでアニメーションを定義し、JavaScript で制御することが一般的でした。しかし、React のようなコンポーネントベースの開発では、スタイルとロジックを同一ファイルで管理することで、保守性と開発効率が大幅に向上します。

CSS-in-JS ライブラリを使用することで、以下のようなメリットが得られます。

  • 動的スタイル制御: props や state に基づくリアルタイムなスタイル変更
  • スコープ分離: コンポーネント単位でのスタイル管理
  • TypeScript 支援: 型安全性の確保とエディタ支援

現代の Web アプリケーションでは、以下のような場面でアニメーションが必要となります。

mermaidflowchart TD
  app[Web アプリケーション] --> ui[UI フィードバック]
  app --> transition[画面遷移]
  app --> interaction[ユーザーインタラクション]
  
  ui --> button[ボタンホバー効果]
  ui --> form[フォーム入力フィードバック]
  
  transition --> page[ページ切り替え]
  transition --> modal[モーダル表示/非表示]
  
  interaction --> scroll[スクロール連動]
  interaction --> loading[ローディング表示]

Emotion の位置づけとアニメーション対応状況

Emotion は、React エコシステムにおける主要な CSS-in-JS ライブラリの一つです。styled-components と並んで高い人気を誇り、特にアニメーション機能においては以下の特徴があります。

項目Emotionstyled-components説明
keyframes APIアニメーション定義の基本機能
CSS プロパティ結合css 関数による柔軟な組み合わせ
パフォーマンスランタイムオーバーヘッドの少なさ
TypeScript 対応型定義の充実度
バンドルサイズ最終バンドルサイズ

Emotion のアニメーション機能は、以下の API を中心に構成されています。

typescript// 基本的な Emotion のインポート
import { css, keyframes } from '@emotion/react';
import styled from '@emotion/styled';

課題

従来の CSS アニメーションとの違いと課題

従来の CSS ファイルでアニメーションを管理する場合、以下のような課題がありました。

グローバルスコープの問題

CSS ファイルで定義したアニメーションは、グローバルスコープに配置されるため、命名衝突や意図しない適用が発生しやすくなります。

css/* 従来の CSS アプローチ */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.button {
  animation: fadeIn 0.3s ease-in-out;
}

この場合、他のコンポーネントでも fadeIn という名前のアニメーションがあると衝突する可能性があります。

動的制御の困難さ

CSS ファイルでは、JavaScript の変数や state を直接参照できないため、動的なアニメーション制御が困難です。

javascript// 従来のアプローチでの動的制御
const button = document.querySelector('.button');
button.style.animationDuration = `${duration}s`;
button.style.animationDelay = `${delay}s`;

JavaScript でのアニメーション制御の難しさ

React コンポーネント内でアニメーションを制御する際に、以下のような課題が発生します。

ライフサイクルとの連携複雑さ

mermaidsequenceDiagram
  participant Component
  participant useEffect
  participant Animation
  participant DOM
  
  Component->>useEffect: マウント
  useEffect->>Animation: アニメーション開始
  Animation->>DOM: スタイル適用
  Component->>useEffect: アンマウント
  useEffect->>Animation: クリーンアップ

パフォーマンスの課題

頻繁な DOM 操作や再レンダリングによるパフォーマンス低下が課題となります。

typescript// 問題のあるパターン例
const [isAnimating, setIsAnimating] = useState(false);

useEffect(() => {
  // 毎回新しいアニメーション定義が作成される
  const animation = keyframes`
    from { transform: translateX(0); }
    to { transform: translateX(100px); }
  `;
}, [isAnimating]); // 依存関係により毎回実行

解決策

Emotion でのアニメーション実装アプローチ

Emotion は、これらの課題を効果的に解決する仕組みを提供しています。

スコープ分離によるアニメーション管理

Emotion では、keyframes を使ってコンポーネントスコープ内でアニメーションを定義できます。

typescript// Emotion によるスコープ分離
import { css, keyframes } from '@emotion/react';

// コンポーネント内でアニメーション定義
const fadeInAnimation = keyframes`
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

動的なアニメーション制御

props や state に基づいた動的なアニメーション制御が簡単に実現できます。

typescript// 動的なアニメーション制御
interface AnimatedButtonProps {
  duration?: number;
  delay?: number;
  isVisible?: boolean;
}

const AnimatedButton: React.FC<AnimatedButtonProps> = ({
  duration = 0.3,
  delay = 0,
  isVisible = false
}) => {
  const buttonStyles = css`
    opacity: ${isVisible ? 1 : 0};
    animation: ${fadeInAnimation} ${duration}s ease-in-out ${delay}s;
    transition: all 0.2s ease;
  `;

  return <button css={buttonStyles}>アニメーションボタン</button>;
};

keyframes と transition の使い分け

Emotion では、異なる種類のアニメーションに対して適切な API を選択することが重要です。

keyframes の使用場面

複雑なアニメーション、複数のキーフレームが必要な場合に使用します。

typescript// 複雑なローディングアニメーション
const spinAnimation = keyframes`
  0% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg) scale(1.2);
  }
  100% {
    transform: rotate(360deg);
  }
`;

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

transition の使用場面

シンプルな状態変化、ホバーエフェクトなどに適しています。

typescript// シンプルなホバーエフェクト
const HoverButton = styled.button`
  background-color: #3498db;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;

  &:hover {
    background-color: #2980b9;
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  }
`;

具体例

基本的な遅延アニメーション

シンプルな delay 指定

最も基本的な遅延アニメーションの実装から始めましょう。

typescript// 基本的な遅延アニメーション
import { css, keyframes } from '@emotion/react';

const fadeInUp = keyframes`
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;
typescript// 遅延付きのアニメーション適用
const DelayedCard: React.FC<{ delay: number }> = ({ delay }) => {
  const cardStyles = css`
    animation: ${fadeInUp} 0.6s ease-out ${delay}s both;
    padding: 20px;
    margin: 10px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  `;

  return (
    <div css={cardStyles}>
      <h3>遅延カード</h3>
      <p>このカードは {delay} 秒後に表示されます。</p>
    </div>
  );
};

複数要素の順次表示

リストアイテムを順次表示するアニメーションパターンです。

typescript// リストアイテムの順次表示
const AnimatedList: React.FC<{ items: string[] }> = ({ items }) => {
  return (
    <div>
      {items.map((item, index) => (
        <DelayedCard
          key={index}
          delay={index * 0.1} // 0.1秒ずつ遅延
        >
          {item}
        </DelayedCard>
      ))}
    </div>
  );
};
typescript// 使用例
const itemList = [
  'アイテム1',
  'アイテム2', 
  'アイテム3',
  'アイテム4'
];

<AnimatedList items={itemList} />

以下の図は、順次表示アニメーションのタイミングを示しています。

mermaidgantt
  title 複数要素の順次表示タイミング
  dateFormat X
  axisFormat %Ls
  
  section アイテム1
  フェードイン :0, 600
  
  section アイテム2
  フェードイン :100, 700
  
  section アイテム3
  フェードイン :200, 800
  
  section アイテム4
  フェードイン :300, 900

図で理解できる要点:

  • 各アイテムは0.1秒(100ms)ずつ遅延して開始
  • アニメーション時間は0.6秒で統一
  • 全体的に波のような表示効果を演出

高度な遷移エフェクト

ホバーアニメーション

マウスホバー時の滑らかな遷移エフェクトを実装します。

typescript// 高度なホバーエフェクト
const InteractiveCard = styled.div`
  padding: 24px;
  border-radius: 12px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  cursor: pointer;
  transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  transform: perspective(1000px) rotateX(0deg) rotateY(0deg);

  &:hover {
    transform: perspective(1000px) rotateX(-10deg) rotateY(10deg) 
               translateY(-10px) scale(1.05);
    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
  }

  &:active {
    transform: perspective(1000px) rotateX(-5deg) rotateY(5deg) 
               translateY(-5px) scale(1.02);
  }
`;
typescript// ホバー時の内部要素アニメーション
const CardContent = styled.div`
  transition: transform 0.3s ease;

  ${InteractiveCard}:hover & {
    transform: translateZ(20px);
  }
`;

const HoverDemo: React.FC = () => {
  return (
    <InteractiveCard>
      <CardContent>
        <h3>インタラクティブカード</h3>
        <p>ホバーしてアニメーションを確認してください</p>
      </CardContent>
    </InteractiveCard>
  );
};

ページ遷移アニメーション

ページ切り替え時のアニメーション実装です。

typescript// ページ遷移アニメーション
const slideInRight = keyframes`
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
`;

const slideOutLeft = keyframes`
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
`;
typescript// ページコンテナコンポーネント
const PageContainer = styled.div<{ isExiting: boolean }>`
  animation: ${props => props.isExiting ? slideOutLeft : slideInRight} 
             0.5s ease-in-out;
  width: 100%;
  height: 100vh;
  padding: 20px;
`;

const PageTransition: React.FC<{ 
  currentPage: string; 
  isTransitioning: boolean 
}> = ({ currentPage, isTransitioning }) => {
  return (
    <PageContainer isExiting={isTransitioning}>
      <h1>{currentPage}</h1>
      <p>ページコンテンツがここに表示されます</p>
    </PageContainer>
  );
};

実践的な応用例

モーダル表示/非表示

モーダルダイアログの開閉アニメーションを実装します。

typescript// モーダルアニメーション
const modalFadeIn = keyframes`
  from {
    opacity: 0;
    transform: scale(0.7) translateY(-50px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
`;

const modalFadeOut = keyframes`
  from {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
  to {
    opacity: 0;
    transform: scale(0.7) translateY(-50px);
  }
`;
typescript// モーダルオーバーレイ
const ModalOverlay = styled.div<{ isVisible: boolean }>`
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: ${props => props.isVisible ? 1 : 0};
  visibility: ${props => props.isVisible ? 'visible' : 'hidden'};
  transition: all 0.3s ease;
`;

// モーダルコンテンツ
const ModalContent = styled.div<{ isVisible: boolean; isExiting: boolean }>`
  background: white;
  padding: 32px;
  border-radius: 16px;
  max-width: 500px;
  width: 90%;
  animation: ${props => 
    props.isExiting ? modalFadeOut : 
    props.isVisible ? modalFadeIn : 'none'
  } 0.4s ease-out forwards;
`;
typescript// モーダルコンポーネントの実装
const AnimatedModal: React.FC<{
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}> = ({ isOpen, onClose, children }) => {
  const [isExiting, setIsExiting] = useState(false);

  const handleClose = () => {
    setIsExiting(true);
    setTimeout(() => {
      setIsExiting(false);
      onClose();
    }, 400); // アニメーション時間と同期
  };

  if (!isOpen && !isExiting) return null;

  return (
    <ModalOverlay isVisible={isOpen && !isExiting} onClick={handleClose}>
      <ModalContent 
        isVisible={isOpen} 
        isExiting={isExiting}
        onClick={e => e.stopPropagation()}
      >
        {children}
        <button onClick={handleClose}>閉じる</button>
      </ModalContent>
    </ModalOverlay>
  );
};

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

複数のローディング表現を実装します。

typescript// 回転ローディング
const spin = keyframes`
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
`;

const SpinLoader = styled.div`
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: ${spin} 1s linear infinite;
`;
typescript// パルスローディング
const pulse = keyframes`
  0% {
    transform: scale(0);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 0;
  }
`;

const PulseLoader = styled.div`
  display: inline-block;
  width: 60px;
  height: 60px;
  position: relative;

  &:after {
    content: '';
    position: absolute;
    width: 60px;
    height: 60px;
    border-radius: 50%;
    background: #3498db;
    animation: ${pulse} 1.2s ease-in-out infinite;
  }
`;
typescript// ドットローディング
const bounce = keyframes`
  0%, 80%, 100% {
    transform: scale(0);
  }
  40% {
    transform: scale(1);
  }
`;

const DotsLoader = styled.div`
  display: inline-block;
  position: relative;
  width: 80px;
  height: 80px;

  div {
    position: absolute;
    top: 33px;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background: #3498db;
    animation: ${bounce} 1.4s ease-in-out infinite both;
  }

  div:nth-child(1) { left: 8px; animation-delay: -0.32s; }
  div:nth-child(2) { left: 32px; animation-delay: -0.16s; }
  div:nth-child(3) { left: 56px; animation-delay: 0s; }
`;

以下の図は、ローディングアニメーションの動作パターンを示しています。

mermaidstateDiagram-v2
  [*] --> 待機状態
  待機状態 --> 読み込み開始
  読み込み開始 --> スピンアニメーション
  読み込み開始 --> パルスアニメーション
  読み込み開始 --> ドットアニメーション
  
  スピンアニメーション --> 読み込み完了
  パルスアニメーション --> 読み込み完了
  ドットアニメーション --> 読み込み完了
  
  読み込み完了 --> [*]

図で理解できる要点:

  • 3つの異なるローディングパターンを並行実行可能
  • 各アニメーションは無限ループで動作
  • 読み込み完了時に統一的に終了処理を実行

まとめ

Emotion を使ったアニメーション遅延・遷移の実装について、基礎から応用まで幅広く解説いたしました。本記事でご紹介した手法を使うことで、以下のようなメリットが得られます。

技術的なメリット

  • コンポーネントスコープでのアニメーション管理により、保守性が向上します
  • TypeScript との連携により、型安全性を確保しながら開発できます
  • props や state との連携により、動的なアニメーション制御が可能になります

ユーザーエクスペリエンスの向上

  • 適切な遅延タイミングにより、情報の階層構造を視覚的に表現できます
  • 滑らかな遷移エフェクトにより、操作に対するフィードバックを提供できます
  • ローディング表示やモーダル操作において、待機時間を快適に演出できます

開発効率の改善

  • keyframes と transition の適切な使い分けにより、効率的な実装が可能です
  • 再利用可能なコンポーネント設計により、一貫性のあるアニメーション表現を実現できます
  • パフォーマンスを考慮した実装パターンにより、スムーズな動作を維持できます

今回学んだ技術を活用して、ユーザーにとって魅力的で使いやすい Web アプリケーションを作成してください。アニメーションは単なる装飾ではなく、ユーザーとのコミュニケーション手段として重要な役割を果たします。

関連リンク