シリーズ ゼロからWebサイトを作ろう! 番外編① / 全3回

CSSアニメーションで
動きをつけよう

本編で完成したcafé SOELのサイトに、ふわっと現れるフェードインやスクロール連動のアニメーションを追加します。CSSだけで実現できる「心地よい動き」の作り方を、一緒に学んでいきましょう。

本編のおさらいと、今回のゴール

📖 本編のおさらい
  • 全10回を通して、café SOELのWebサイトをゼロから完成させた
  • HTML・CSS・レスポンシブデザイン・SEO・アクセシビリティまで対応済み
  • 第7回でtransitionとhover効果を追加し、ナビの下線アニメーションやカードの浮き上がりを実装した

本編10回、おつかれさまでした。あなたの手元には、レスポンシブ対応もSEO対策も済んだ、しっかりとしたWebサイトがあるはずです。

この番外編では、そのサイトにもうひとつ「体験の質」を加えます。ページを開いたときにコンテンツがふわっと現れたり、スクロールに合わせてセクションが登場したり。そうした動きがあると、サイトの印象はぐっと洗練されたものになります。

🎯 今回のゴール
  • @keyframes の書き方を理解し、フェードインアニメーションを作る
  • スクロール連動で要素が現れるアニメーションの仕組みを知る
  • ホバー演出をさらに磨き上げる
  • ハンバーガーメニューの×変形アニメーションを理解する
  • prefers-reduced-motion でアニメーションのアクセシビリティに対応する

第7回で学んだ transition は「AからBへの変化」を滑らかにする仕組みでした。今回学ぶ @keyframes アニメーションは、もっと自由度が高い仕組みです。途中の経過点を細かく指定できるので、複雑な動きも表現できます。

とはいえ、大事なのは「派手に動かすこと」ではありません。

よいアニメーションは、ユーザーが「気づかないくらい自然」であること。動きそのものではなく、動きが生む心地よさを設計する。

この考え方を軸に、進めていきましょう。

@keyframes — アニメーションの設計図

CSSアニメーションの核になるのが @keyframes です。「時間の流れの中で、どのプロパティをどう変化させるか」を定義するルールで、いわばアニメーションの設計図のようなものです。

基本の書き方

まずは最もシンプルな例を見てみましょう。要素を「透明な状態から、ふわっと現れる」フェードインです。

css/style.css — @keyframes の基本構文
/* アニメーションの設計図を定義 */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

from が開始時点、to が終了時点の状態です。この例では、透明度(opacity)が 0 から 1 へ変化します。つまり「見えない状態から、完全に見える状態へ」という動きですね。

この設計図を実際の要素に適用するには、animation プロパティを使います。

css/style.css — animation プロパティで適用
.hero-content {
  animation: fadeIn 0.8s ease-out both;
}

ここで指定しているのは、順番に「どの設計図を使うか」「何秒かけるか」「どんな緩急で動かすか」「終了後にどうするか」の4つです。

💡 KANONのワンポイント
animation の各値を整理しておきますね。

animation: [name] [duration] [timing-function] [fill-mode];

name@keyframes で付けた名前(例:fadeIn
duration — 再生時間(例:0.8s
timing-function — 緩急の曲線(例:ease-out
fill-mode — 再生前後の状態を保持するか(both が便利)

他にも delay(開始を遅らせる)、iteration-count(繰り返し回数)なども指定できます。今回はよく使うものだけ扱います。

from / to ではなく % で指定する

fromto は、実は 0%100% の省略形です。途中に経過点を追加したい場合は、パーセンテージで書きます。

css/style.css — 途中の経過点を指定
@keyframes fadeInUp {
  0% {
    opacity: 0;
    transform: translateY(24px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

この fadeInUp は、下から24pxの位置からスッと上がりながらフェードインする動きです。opacitytransform を同時に変化させるので、ただ現れるだけよりも上品な印象になります。

📌 覚えておこう
アニメーションで動かすプロパティは、transformopacity に絞るのが鉄則です。この2つはブラウザのGPU(グラフィック処理装置)が効率的に処理できるため、60fpsの滑らかな動きが実現できます。widthmargin をアニメーションさせると、ブラウザがページ全体のレイアウトを再計算することになり、カクつきの原因になります。

それでは、実際にデモで動きを確認してみましょう。ボタンを押すと fadeInUp が再生されます。

▶ Live Demo — fadeInUp アニメーション

下から浮き上がるように現れましたか? 移動距離はたった24px、再生時間は0.8秒。控えめですが、だからこそ心地よい動きになっています。

ヒーローセクションにフェードインを加える

では、café SOELのサイトに実際にアニメーションを追加していきましょう。まずはヒーローセクション。ページを開いた瞬間に目に入る場所なので、ここにフェードインを加えると第一印象がぐっと良くなります。

css/style.css の末尾に、以下のコードを追加してください。

css/style.css — ヒーローのフェードインアニメーション
/* ========== Animation Keyframes ========== */

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ========== Hero Animation ========== */

.hero-sub {
  animation: fadeIn 1s ease-out 0.2s both;
}

.hero-title {
  animation: fadeInUp 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.5s both;
}

.hero-cta {
  animation: fadeIn 0.8s ease-out 1s both;
}

3つの要素に、それぞれ異なるタイミングでアニメーションを設定しています。ポイントは delay(遅延)の値です。

.hero-sub(サブタイトル)が0.2秒後に静かにフェードイン。続いて .hero-title(メインタイトル)が0.5秒後に下からスッと浮き上がる。最後に .hero-cta(Scrollボタン)が1秒後にふわっと現れる。この「順番に登場する」演出を、スタッガー(stagger=時間差)と呼びます。

💡 KANONのワンポイント
cubic-bezier(0.22, 1, 0.36, 1) は、最初にすっと加速して、終盤でゆるやかに減速するカーブです。ease-out よりも自然な「落ち着き」があるので、フェードインの動きによく合います。私がよく使うお気に入りのカーブです。VS Codeで cubic-bezier と入力すると、さまざまなプリセットが表示されるので、ぜひ試してみてくださいね。

保存してブラウザをリロードしてみましょう。ヒーローセクションのテキストが順番にふわっと現れるはずです。

animation-delay で時間差を作るコツ

スタッガーを設定するとき、要素間の時間差は 0.1〜0.3秒くらいが心地よいです。差が大きすぎると、ユーザーは「待たされている」と感じてしまいます。

もうひとつ大事なのが、アニメーション全体の長さ。ページを開いて最後の要素が出揃うまでに2秒以上かかると、長く感じます。ヒーローで使うアニメーションは、全体で1.5〜2秒以内に収めるのが目安です。

▶ Live Demo — スタッガー(時間差)の効果

5つのボックスが順番に現れましたか? 各ボックスの間隔は0.08秒。ほんのわずかな差ですが、「一斉に現れる」のとは印象がまったく違いますよね。

スクロール連動アニメーション — 要素が画面に入ったら動かす

ヒーロー以外のセクションにも、スクロールに合わせて要素が現れるアニメーションを加えてみましょう。ページを下にスクロールしていくと、コンセプトやメニューのセクションがふわっと登場する、あの演出です。

仕組みを理解する

スクロール連動アニメーションは、大きく分けて2つのアプローチがあります。

ひとつは、JavaScriptの Intersection Observer API を使う方法。要素が画面内に入ったタイミングを検知して、CSSクラスを付与する仕組みです。番外編②のJavaScript回で実装するので、今回はCSSの準備だけしておきます。

もうひとつは、CSSの animation-timeline を使う方法。JavaScriptなしでスクロール連動アニメーションが実現できる新しい仕様です。Chrome、Edge、Safariではすでに使えますが、Firefoxではまだ標準で有効になっていないため、今の段階ではすべてのブラウザで確実に動くIntersection Observerのアプローチを採用します。

📌 覚えておこう
CSSの animation-timeline: view() は、将来的にスクロール連動アニメーションの主流になる可能性が高い仕様です。すべてのブラウザが対応したら、JavaScriptなしで同じことができるようになります。「こういう仕様が進んでいる」ということだけ、頭の片隅に置いておいてくださいね。

CSSの準備 — 「待機状態」と「登場状態」を作る

考え方はシンプルです。最初は要素を透明にしておき、画面に入ったときにクラスを追加して、アニメーションを再生する。そのために、まず「まだ見えていない状態」のCSSを書きます。

css/style.css — スクロールアニメーションの準備
/* ========== Scroll Animation ========== */

/* 待機状態:まだ画面に入っていない */
.fade-in-up {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

/* 登場状態:画面に入ったらこの状態になる */
.fade-in-up.is-visible {
  opacity: 1;
  transform: translateY(0);
}

ここでは @keyframes ではなく transition を使っています。理由は、クラスの付け外しによる「AからBへの変化」なので、transition のほうがシンプルに書けるからです。

使い分けの目安を整理しておきましょう。

transition は「状態Aから状態Bへの変化」に向いています。hover効果や、クラスの追加・削除で見た目が切り替わる場面。一方、@keyframes は「途中の経過を細かく制御したい」ときや「ページ読み込み時に自動で再生したい」ときに使います。

HTMLにクラスを追加する

アニメーションを付けたい要素に、fade-in-up クラスを追加します。index.html を開いて、以下のセクションのclass属性に追記してください。

index.html — クラスを追加する場所
<!-- コンセプトセクション -->
<div class="concept-inner fade-in-up">

<!-- メニューのセクションヘッダー -->
<div class="section-header fade-in-up">

<!-- メニューグリッド -->
<div class="menu-grid fade-in-up">

<!-- ギャラリーのセクションヘッダー -->
<div class="section-header fade-in-up">

<!-- ギャラリーグリッド -->
<div class="gallery-grid fade-in-up">

<!-- アクセスセクション -->
<div class="access-inner fade-in-up">

既存のクラス名はそのまま残して、半角スペースで区切って fade-in-up を追加するだけです。

⚠️ よくあるミス
class="concept-inner"class="fade-in-up" に置き換えてしまうと、既存のレイアウトが崩れます。必ず class="concept-inner fade-in-up" のように、既存のクラスに追加してくださいね。

この時点でブラウザを確認すると、fade-in-up を追加した要素はすべて透明で見えなくなっているはずです。is-visible クラスが付いていないので、待機状態のままだからですね。

JavaScriptで is-visible クラスを付与する処理は、番外編②で実装します。今すぐ動作を確認したい方は、ブラウザのデベロッパーツールで要素を選んで手動で is-visible クラスを追加してみてください。ふわっと現れるはずです。

🔰 初心者向け補足
「番外編②まで待てない!」という方のために、最小限のJavaScriptを先にお見せしますね。index.html</body> の直前に以下を追加すると、スクロール連動が動きます。ただし詳しい仕組みは番外編②で解説するので、今は「おまじない」として使ってください。
index.html — </body> の直前に追加(先取りコード)
<script>
  // スクロール連動アニメーション
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('is-visible');
      }
    });
  }, { threshold: 0.15 });

  document.querySelectorAll('.fade-in-up').forEach((el) => {
    observer.observe(el);
  });
</script>

これで、各セクションが画面の15%分見えたタイミングで is-visible クラスが追加され、フェードインアニメーションが再生されます。保存してスクロールしてみてください。ふわっと現れましたか?

ホバー演出をさらに磨く

第7回で、メニューカードの浮き上がりやナビリンクの下線アニメーションを追加しましたね。ここでは、その演出をもう少しだけ洗練させます。

カードホバーのシャドウを滑らかに

第7回で書いた .menu-card:hover のスタイルを振り返ってみましょう。

css/style.css — 第7回で書いたスタイル(確認用)
.menu-card {
  transition: transform var(--transition-base), box-shadow var(--transition-base);
}

.menu-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
}

ここに、より繊細な影の変化を加えてみましょう。通常時にもごく薄い影を入れておくと、ホバー時の変化がより滑らかに見えます。

css/style.css — カードのシャドウを改善
/* .menu-card の既存スタイルに追加 */
.menu-card {
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
  /* ↑ この1行を追加。通常時のごく薄い影 */
}

通常時の box-shadow がゼロだと、ホバーしたときに「影が突然現れる」ように見えることがあります。最初からごく薄い影をつけておくと、変化がなめらかになります。

▶ Live Demo — カードのホバー演出
Hand Drip Coffee

カーソルを載せてみてください

カードがふわっと浮き上がり、内側の画像がわずかにズームする。この2つの動きが同時に起きることで、カードが「生きている」ような印象を与えます。

cubic-bezier を変えるだけで印象が変わる

第7回では var(--transition-base)(0.3s ease)を使っていましたが、cubic-bezier を変えるだけで動きの印象は大きく変わります。

css/style.css — ホバーの緩急を変更(オプション)
/* :root の変数を変更する場合 */
:root {
  --transition-base: 0.35s cubic-bezier(0.22, 1, 0.36, 1);
  /* 元の値:0.3s ease */
}

この cubic-bezier(0.22, 1, 0.36, 1) は「素早く始まって、ゆるやかに着地する」カーブ。現実世界の物体が減速するような自然な動きになります。お好みで試してみてくださいね。変えなくても問題ありません。

ハンバーガーメニューの×変形を理解する

第7回で、ハンバーガーメニューのバーが×印に変形するCSSをすでに書きました。ただ、あのとき「なぜこう書くと×になるのか」を詳しく説明していなかったので、ここで紐解いておきましょう。

まず、ハンバーガーメニューは3本の横棒(バー)でできています。

index.html — ハンバーガーメニューのHTML(第6回で追加済み)
<button class="menu-toggle" aria-label="メニューを開く" aria-expanded="false">
  <span class="menu-toggle-bar"></span>
  <span class="menu-toggle-bar"></span>
  <span class="menu-toggle-bar"></span>
</button>

この3本のバーを×印にするには、こう考えます。

1本目のバーを中央まで下げて、45度回転する。3本目のバーを中央まで上げて、-45度回転する。2本目のバーは透明にして消す。すると、交差した2本の線=×印が現れます。

css/style.css — 第7回で書いたCSS(復習)
.menu-toggle-bar {
  transition: transform var(--transition-base),
              opacity var(--transition-base);
}

/* 1本目:中央まで下がって45度回転 */
.menu-toggle.is-open .menu-toggle-bar:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

/* 2本目:透明になって消える */
.menu-toggle.is-open .menu-toggle-bar:nth-child(2) {
  opacity: 0;
}

/* 3本目:中央まで上がって-45度回転 */
.menu-toggle.is-open .menu-toggle-bar:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

translateY(7px) の「7px」は、バーの高さ(2px)+ バー間のgap(5px)= 7px。つまり「1本分の間隔ぶん移動する」という計算です。この値は、ご自身のバーの太さや間隔に合わせて調整してくださいね。

▶ Live Demo — ハンバーガー ⇄ × の変形

クリックして切り替え

クリックするたびに、三本線と×印がスムーズに切り替わりましたか? transition が付いているおかげで、transform の変化がアニメーションとして表現されます。瞬間的に変わるのではなく、バーが「動いて」×になる。これだけで、UIの品質が一段上がります。

💡 KANONのワンポイント
ハンバーガーメニューの開閉は、番外編②のJavaScriptで .is-open クラスを付け替える処理を実装します。CSSだけでは「クリックしたらクラスを追加する」ということはできないので、JavaScriptとの連携が必要です。CSSはあくまで「この状態のときにどう見えるか」を担当する。役割分担がはっきりしていますよね。

prefers-reduced-motion — 動きが苦手な人への配慮

ここまでアニメーションを追加してきましたが、忘れてはいけないことがあります。すべてのユーザーがアニメーションを心地よいと感じるわけではない、ということです。

前庭障害(めまいや乗り物酔いを起こしやすい疾患)のある方にとって、画面の動きは吐き気や頭痛の原因になることがあります。また、注意欠如の傾向がある方には、動きが集中の妨げになる場合もあります。

そのために用意されているのが、prefers-reduced-motion というメディアクエリです。ユーザーのデバイス設定で「視覚効果を減らす」がオンになっている場合、CSSでアニメーションを控えめにできます。

css/style.css — アクセシビリティ対応を追加
/* ========== Reduced Motion ========== */

@media (prefers-reduced-motion: reduce) {
  /* ヒーローのアニメーションを無効化 */
  .hero-sub,
  .hero-title,
  .hero-cta {
    animation: none;
  }

  /* スクロールアニメーションを無効化 */
  .fade-in-up {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* ホバー時のtransformを控えめに */
  .menu-card:hover {
    transform: none;
  }

  .concept-image:hover img,
  .menu-card:hover .menu-card-image img,
  .gallery-item:hover img {
    transform: none;
  }
}

「動きを減らす」であって「すべて消す」ではないのがポイントです。ホバー時の色変化やフォーカスの表示はそのまま残しています。情報としての変化は維持しつつ、空間的な動き(移動・拡大・回転)だけを止める。これが prefers-reduced-motion の正しい使い方です。

アクセシビリティは「おまけ」ではなく、設計に最初から組み込むもの。アニメーションを加えるときは、同時にそれを止める手段も用意する。

🔰 初心者向け補足
動作確認の方法をお伝えしますね。macOSなら「システム環境設定 → アクセシビリティ → ディスプレイ → 視覚効果を減らす」をオンに。Windowsなら「設定 → アクセシビリティ → 視覚効果 → アニメーション効果」をオフにすると、prefers-reduced-motion: reduce が有効になります。Chromeのデベロッパーツールでも、レンダリングタブからエミュレートできます。

パフォーマンスを意識する

最後に、アニメーションのパフォーマンスについて少し触れておきます。動きが「カクつく」のは、技術的に何が起きているかを知っておくと、自分でトラブルシューティングできるようになります。

なぜ transform と opacity だけを推奨するのか

ブラウザが画面を表示するまでには、大きく分けて「レイアウト → ペイント → コンポジット」という3つのステップがあります。

widthmargin を変化させると、最初の「レイアウト」からやり直しになります。ページ上の他の要素の位置も再計算されるので、処理が重くなります。

background-colorbox-shadow の変化は「ペイント」のやり直し。レイアウトよりは軽いですが、それでも負荷があります。

一方、transformopacity は「コンポジット」の段階だけで完結します。GPUが効率的に処理してくれるので、60fps(1秒間に60コマ)の滑らかな動きが実現できるのです。

💡 KANONのワンポイント
私が駆け出しの頃、メニューの高さを height: 0 から height: auto にアニメーションさせようとして、ガクガクの動きになったことがあります。原因はまさにこれで、height の変化がレイアウトの再計算を引き起こしていたんですね。代わりに transform: scaleY() を使うか、max-height で大きめの値を指定する方法に切り替えたら、滑らかに動くようになりました。

will-change は「ここぞ」のときだけ

will-change というCSSプロパティがあります。「この要素はこのプロパティが変化しますよ」とブラウザに事前に伝えることで、最適化の準備をしてもらう仕組みです。

css/style.css — will-change の使い方
.menu-card {
  will-change: transform;
}

ただし、これはあくまで「最終手段」です。ブラウザは要素ごとにメモリを確保するため、たくさんの要素に will-change を付けると逆にパフォーマンスが悪化します。「特定のアニメーションがカクつく」と感じたときだけ、ピンポイントで使ってください。

今回のcafé SOELサイトでは、transformopacity だけをアニメーションさせているので、will-change を明示的に書く必要はほぼありません。ブラウザが自動で最適化してくれます。

今回追加したコードのまとめ

この番外編で css/style.css に追加したコードをまとめておきます。以下を、ファイルの末尾に追記してください。

css/style.css — 番外編①で追加するコード(まとめ)
/* ===================================
   番外編①:CSSアニメーション
   =================================== */

/* ---------- Keyframes ---------- */

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ---------- Hero Animation ---------- */

.hero-sub {
  animation: fadeIn 1s ease-out 0.2s both;
}

.hero-title {
  animation: fadeInUp 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.5s both;
}

.hero-cta {
  animation: fadeIn 0.8s ease-out 1s both;
}

/* ---------- Scroll Animation ---------- */

.fade-in-up {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

.fade-in-up.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- Card Shadow Enhancement ---------- */

/* .menu-card に追加 */
.menu-card {
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}

/* ---------- Reduced Motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .hero-sub,
  .hero-title,
  .hero-cta {
    animation: none;
  }

  .fade-in-up {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .menu-card:hover {
    transform: none;
  }

  .concept-image:hover img,
  .menu-card:hover .menu-card-image img,
  .gallery-item:hover img {
    transform: none;
  }
}

また、index.html では以下の変更を行いました。

index.html — 変更箇所の一覧
<!-- 以下の要素に fade-in-up クラスを追加 -->
<div class="concept-inner fade-in-up">
<div class="section-header fade-in-up">  <!-- メニュー -->
<div class="menu-grid fade-in-up">
<div class="section-header fade-in-up">  <!-- ギャラリー -->
<div class="gallery-grid fade-in-up">
<div class="access-inner fade-in-up">

<!-- </body> の直前にスクロール監視のscriptを追加(番外編②で詳しく解説) -->

保存してブラウザで確認してみてください。ページを開くとヒーローがふわっと登場し、スクロールするとセクションが順に現れるはずです。

今回のまとめ

  • @keyframes でアニメーションの設計図を作り、animation プロパティで要素に適用する
  • animation-delay で時間差(スタッガー)をつけると、要素が順番に登場する演出ができる
  • スクロール連動アニメーションは「待機状態のCSS」+「JavaScriptによるクラス付与」で実現する
  • アニメーションで動かすプロパティは transformopacity に絞ると、60fpsの滑らかな動きが得られる
  • prefers-reduced-motion メディアクエリで、動きが苦手なユーザーへの配慮を忘れない
  • will-change は「カクつきが出たときの最終手段」。むやみに使わない
  • よいアニメーションは「気づかないくらい自然」で、「短く・控えめ」であること

ここまでで、CSSだけで実現できるアニメーションの基礎はすべてカバーしました。次の番外編②では、JavaScriptを使ってハンバーガーメニューの開閉やスムーススクロールなど、インタラクション(操作への反応)を実装していきます。今回準備したスクロールアニメーションも、番外編②で仕組みを詳しく解説しますので、楽しみにしていてくださいね。