CSS実践入門第7回:アニメーション・トランジション実装 - 滑らかな動きでユーザー体験を向上

CSSアニメーションとトランジションを使って、魅力的で滑らかな動きのあるWebサイトを作成する方法を詳しく解説。パフォーマンスを考慮した実装テクニックも紹介します。

現代のWebサイトにおいて、適切に配置されたアニメーションは、ユーザーの注意を引き、操作の楽しさを演出し、全体的なユーザーエクスペリエンスを大幅に向上させます。今回は、CSSを使った効果的なアニメーションとトランジションの実装方法を詳しく解説します。

この記事で学べること

  • CSSトランジションとアニメーションの基本原理
  • 実用的なホバーエフェクトとインタラクションの実装
  • パフォーマンスを考慮したアニメーション設計
  • ローディングアニメーションとマイクロインタラクション
  • 複雑なアニメーションシーケンスの構築方法

1. トランジションの基礎

基本的なトランジション

トランジションは、CSSプロパティの変化を滑らかに表現する最も基本的な手法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 基本的なトランジション構文 */
.transition-basic {
    transition: property duration timing-function delay;
}

/* 実用例:ボタンのホバーエフェクト */
.button {
    background-color: #3498db;
    color: white;
    padding: 12px 24px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 16px;
    
    /* 複数プロパティの同時トランジション */
    transition: background-color 0.3s ease,
                transform 0.3s ease,
                box-shadow 0.3s ease;
}

.button:hover {
    background-color: #2980b9;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}

.button:active {
    transform: translateY(0);
    transition-duration: 0.1s;
}

高度なタイミング関数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* カスタムイージング関数 */
.custom-easing {
    transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* 実用的なイージングパターン */
.ease-patterns {
    /* バウンスエフェクト */
    --bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
    
    /* エラスティック */
    --elastic: cubic-bezier(0.175, 0.885, 0.32, 1.275);
    
    /* 滑らかなスタート */
    --smooth-start: cubic-bezier(0.25, 0.46, 0.45, 0.94);
    
    /* 急速な終了 */
    --quick-end: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}

/* 使用例 */
.card {
    transform: scale(1);
    transition: transform 0.4s var(--bounce);
}

.card:hover {
    transform: scale(1.05);
}

2. 実用的なホバーエフェクト

カードコンポーネントのアニメーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
.animated-card {
    background: white;
    border-radius: 12px;
    padding: 24px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
    transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    cursor: pointer;
    position: relative;
    overflow: hidden;
}

/* グラデーション背景の動的変化 */
.animated-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(135deg, 
        rgba(255, 255, 255, 0) 0%, 
        rgba(255, 255, 255, 0.1) 50%, 
        rgba(255, 255, 255, 0) 100%);
    transform: translateX(-100%);
    transition: transform 0.6s ease;
    z-index: 1;
}

.animated-card:hover {
    transform: translateY(-8px) scale(1.02);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
}

.animated-card:hover::before {
    transform: translateX(100%);
}

/* カード内要素のアニメーション */
.card-title {
    transition: color 0.3s ease;
}

.card-image {
    transition: transform 0.5s ease;
}

.animated-card:hover .card-title {
    color: #3498db;
}

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

ナビゲーションメニューのアニメーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
.nav-menu {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
}

.nav-item {
    position: relative;
    margin: 0 20px;
}

.nav-link {
    display: block;
    padding: 10px 0;
    text-decoration: none;
    color: #333;
    font-weight: 500;
    position: relative;
    overflow: hidden;
    transition: color 0.3s ease;
}

/* アンダーラインアニメーション */
.nav-link::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 2px;
    background: linear-gradient(90deg, #3498db, #9b59b6);
    transform: translateX(-100%);
    transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.nav-link:hover {
    color: #3498db;
}

.nav-link:hover::after {
    transform: translateX(0);
}

/* アクティブ状態 */
.nav-link.active::after {
    transform: translateX(0);
}

3. キーフレームアニメーション

基本的なキーフレーム

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* フェードイン効果 */
@keyframes fadeIn {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* 使用例 */
.fade-in-element {
    animation: fadeIn 0.6s ease forwards;
}

/* 複雑なタイミングの指定 */
@keyframes slideInFromLeft {
    0% {
        opacity: 0;
        transform: translateX(-100px);
    }
    50% {
        opacity: 0.8;
        transform: translateX(10px);
    }
    100% {
        opacity: 1;
        transform: translateX(0);
    }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* スピナーローダー */
@keyframes spin {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

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

/* パルスローダー */
@keyframes pulse {
    0% {
        transform: scale(0.95);
        box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.7);
    }
    70% {
        transform: scale(1);
        box-shadow: 0 0 0 10px rgba(52, 152, 219, 0);
    }
    100% {
        transform: scale(0.95);
        box-shadow: 0 0 0 0 rgba(52, 152, 219, 0);
    }
}

.loader-pulse {
    width: 60px;
    height: 60px;
    background: #3498db;
    border-radius: 50%;
    animation: pulse 2s infinite;
}

/* ドットローダー */
@keyframes dotBounce {
    0%, 80%, 100% {
        transform: scale(0);
        opacity: 0.5;
    }
    40% {
        transform: scale(1);
        opacity: 1;
    }
}

.loader-dots {
    display: flex;
    gap: 4px;
}

.loader-dots div {
    width: 8px;
    height: 8px;
    background: #3498db;
    border-radius: 50%;
    animation: dotBounce 1.4s infinite ease-in-out both;
}

.loader-dots div:nth-child(1) { animation-delay: -0.32s; }
.loader-dots div:nth-child(2) { animation-delay: -0.16s; }
.loader-dots div:nth-child(3) { animation-delay: 0s; }

4. 複雑なアニメーションシーケンス

段階的な要素の表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 順次表示アニメーション */
@keyframes slideInSequence {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.sequence-container .item {
    opacity: 0;
    animation: slideInSequence 0.6s ease forwards;
}

.sequence-container .item:nth-child(1) { animation-delay: 0.1s; }
.sequence-container .item:nth-child(2) { animation-delay: 0.2s; }
.sequence-container .item:nth-child(3) { animation-delay: 0.3s; }
.sequence-container .item:nth-child(4) { animation-delay: 0.4s; }
.sequence-container .item:nth-child(5) { animation-delay: 0.5s; }

/* CSS変数を使用した動的遅延 */
.dynamic-sequence .item {
    animation: slideInSequence 0.6s ease forwards;
    animation-delay: calc(var(--index) * 0.1s);
}

モーダルアニメーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* モーダルの開閉アニメーション */
@keyframes modalFadeIn {
    from {
        opacity: 0;
        visibility: hidden;
    }
    to {
        opacity: 1;
        visibility: visible;
    }
}

@keyframes modalSlideIn {
    from {
        opacity: 0;
        transform: translateY(-50px) scale(0.9);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    
    /* 初期状態 */
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease;
}

.modal-overlay.active {
    opacity: 1;
    visibility: visible;
}

.modal-content {
    background: white;
    border-radius: 12px;
    padding: 32px;
    max-width: 500px;
    width: 90%;
    position: relative;
    
    /* 初期状態 */
    transform: translateY(-50px) scale(0.9);
    opacity: 0;
    transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.modal-overlay.active .modal-content {
    transform: translateY(0) scale(1);
    opacity: 1;
}

5. パフォーマンス最適化

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* GPU最適化されたプロパティの使用 */
.gpu-optimized {
    /* transform と opacity は GPU で処理される */
    transform: translateZ(0); /* ハードウェアアクセラレーションを強制 */
    will-change: transform, opacity; /* ブラウザに最適化のヒントを提供 */
}

/* 避けるべきプロパティのアニメーション */
.avoid-these {
    /* これらは CPU 処理で重い */
    /* width, height, top, left, margin, padding など */
}

/* 代替案:transform を使用 */
.use-transform-instead {
    /* width の代わりに scaleX */
    transform: scaleX(0.5);
    
    /* left の代わりに translateX */
    transform: translateX(100px);
}

メディアクエリによる最適化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* モーションを減らしたいユーザーへの配慮 */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* デバイス性能に応じた最適化 */
@media (hover: hover) and (pointer: fine) {
    /* デスクトップでのみホバーエフェクトを有効化 */
    .hover-effect:hover {
        transform: scale(1.05);
        transition: transform 0.3s ease;
    }
}

@media (max-width: 768px) {
    /* モバイルではアニメーションを簡素化 */
    .complex-animation {
        animation: none;
    }
    
    .mobile-simple-animation {
        transition: opacity 0.2s ease;
    }
}

6. JavaScript 連携アニメーション

Intersection Observer との連携

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* 初期状態 */
.animate-on-scroll {
    opacity: 0;
    transform: translateY(50px);
    transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

/* アクティブ状態 */
.animate-on-scroll.in-view {
    opacity: 1;
    transform: translateY(0);
}

/* 様々な方向からのアニメーション */
.slide-left {
    transform: translateX(-50px);
}

.slide-right {
    transform: translateX(50px);
}

.slide-up {
    transform: translateY(50px);
}

.slide-down {
    transform: translateY(-50px);
}

.fade-scale {
    transform: scale(0.9);
}

.animate-on-scroll.in-view.slide-left,
.animate-on-scroll.in-view.slide-right,
.animate-on-scroll.in-view.slide-up,
.animate-on-scroll.in-view.slide-down {
    transform: translateX(0) translateY(0);
}

.animate-on-scroll.in-view.fade-scale {
    transform: scale(1);
}

対応するJavaScript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Intersection Observer による scroll アニメーション
class ScrollAnimator {
    constructor() {
        this.observerOptions = {
            root: null,
            rootMargin: '0px 0px -100px 0px',
            threshold: 0.1
        };
        
        this.observer = new IntersectionObserver(
            this.handleIntersection.bind(this),
            this.observerOptions
        );
        
        this.init();
    }
    
    init() {
        const elements = document.querySelectorAll('.animate-on-scroll');
        elements.forEach(el => {
            this.observer.observe(el);
        });
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                entry.target.classList.add('in-view');
                
                // 一度アニメーションしたら観測を停止(パフォーマンス向上)
                this.observer.unobserve(entry.target);
            }
        });
    }
}

// 初期化
document.addEventListener('DOMContentLoaded', () => {
    new ScrollAnimator();
});

7. 高度なアニメーション効果

パーティクル風エフェクト

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@keyframes float {
    0%, 100% {
        transform: translateY(0px) rotate(0deg);
        opacity: 1;
    }
    50% {
        transform: translateY(-20px) rotate(180deg);
        opacity: 0.8;
    }
}

.particle {
    position: absolute;
    width: 4px;
    height: 4px;
    background: radial-gradient(circle, #3498db 0%, transparent 70%);
    border-radius: 50%;
    animation: float 3s ease-in-out infinite;
}

.particle:nth-child(1) { 
    left: 10%; 
    animation-delay: 0s; 
    animation-duration: 3s; 
}
.particle:nth-child(2) { 
    left: 20%; 
    animation-delay: 0.5s; 
    animation-duration: 4s; 
}
.particle:nth-child(3) { 
    left: 30%; 
    animation-delay: 1s; 
    animation-duration: 2.5s; 
}

グラデーション背景のアニメーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@keyframes gradientShift {
    0% {
        background-position: 0% 50%;
    }
    50% {
        background-position: 100% 50%;
    }
    100% {
        background-position: 0% 50%;
    }
}

.animated-gradient {
    background: linear-gradient(
        45deg,
        #3498db,
        #9b59b6,
        #e74c3c,
        #f39c12,
        #2ecc71
    );
    background-size: 300% 300%;
    animation: gradientShift 8s ease infinite;
}

8. 実用的なコンポーネント例

アニメーション付きプログレスバー

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
.progress-container {
    width: 100%;
    height: 8px;
    background: #f0f0f0;
    border-radius: 4px;
    overflow: hidden;
    position: relative;
}

.progress-bar {
    height: 100%;
    background: linear-gradient(90deg, #3498db, #2ecc71);
    border-radius: 4px;
    transform: translateX(-100%);
    transition: transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
    position: relative;
    overflow: hidden;
}

.progress-bar::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background: linear-gradient(
        90deg,
        transparent,
        rgba(255, 255, 255, 0.4),
        transparent
    );
    animation: shimmer 2s infinite;
}

@keyframes shimmer {
    0% {
        transform: translateX(-100%);
    }
    100% {
        transform: translateX(100%);
    }
}

/* JavaScript で制御 */
.progress-bar.active {
    transform: translateX(calc(-100% + var(--progress, 0%)));
}

まとめ

CSSアニメーションとトランジションを効果的に活用することで、静的なWebページを魅力的でインタラクティブなユーザーエクスペリエンスに変換できます。

重要なポイント

  • パフォーマンス重視: GPU最適化されたプロパティ(transform、opacity)を優先使用
  • ユーザー配慮: prefers-reduced-motion でアクセシビリティに配慮
  • 適切なタイミング: イージング関数で自然な動きを演出
  • 段階的な実装: 基本的なトランジションから複雑なキーフレームアニメーションへ

実装時の注意点

  • アニメーションの過度な使用は避け、目的を明確にする
  • デバイスの性能に応じてアニメーションを最適化
  • ローディング中や状態変化の視覚的フィードバックを提供
  • モバイルデバイスではシンプルなアニメーションを心がける

次回の記事では、「CSS実践入門第8回:ウィジェットシステム構築」について解説予定です。再利用可能なコンポーネントの設計と実装方法を詳しく紹介します。


関連記事:

技術ネタ、趣味や備忘録などを書いているブログです
Hugo で構築されています。
テーマ StackJimmy によって設計されています。