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 と並んで高い人気を誇り、特にアニメーション機能においては以下の特徴があります。
項目 | Emotion | styled-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 アプリケーションを作成してください。アニメーションは単なる装飾ではなく、ユーザーとのコミュニケーション手段として重要な役割を果たします。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来