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 です。「時間の流れの中で、どのプロパティをどう変化させるか」を定義するルールで、いわばアニメーションの設計図のようなものです。
基本の書き方
まずは最もシンプルな例を見てみましょう。要素を「透明な状態から、ふわっと現れる」フェードインです。
/* アニメーションの設計図を定義 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
from が開始時点、to が終了時点の状態です。この例では、透明度(opacity)が 0 から 1 へ変化します。つまり「見えない状態から、完全に見える状態へ」という動きですね。
この設計図を実際の要素に適用するには、animation プロパティを使います。
.hero-content {
animation: fadeIn 0.8s ease-out both;
}
ここで指定しているのは、順番に「どの設計図を使うか」「何秒かけるか」「どんな緩急で動かすか」「終了後にどうするか」の4つです。
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 ではなく % で指定する
from と to は、実は 0% と 100% の省略形です。途中に経過点を追加したい場合は、パーセンテージで書きます。
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(24px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
この fadeInUp は、下から24pxの位置からスッと上がりながらフェードインする動きです。opacity と transform を同時に変化させるので、ただ現れるだけよりも上品な印象になります。
それでは、実際にデモで動きを確認してみましょう。ボタンを押すと fadeInUp が再生されます。
下から浮き上がるように現れましたか? 移動距離はたった24px、再生時間は0.8秒。控えめですが、だからこそ心地よい動きになっています。
ヒーローセクションにフェードインを加える
では、café SOELのサイトに実際にアニメーションを追加していきましょう。まずはヒーローセクション。ページを開いた瞬間に目に入る場所なので、ここにフェードインを加えると第一印象がぐっと良くなります。
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=時間差)と呼びます。
保存してブラウザをリロードしてみましょう。ヒーローセクションのテキストが順番にふわっと現れるはずです。
animation-delay で時間差を作るコツ
スタッガーを設定するとき、要素間の時間差は 0.1〜0.3秒くらいが心地よいです。差が大きすぎると、ユーザーは「待たされている」と感じてしまいます。
もうひとつ大事なのが、アニメーション全体の長さ。ページを開いて最後の要素が出揃うまでに2秒以上かかると、長く感じます。ヒーローで使うアニメーションは、全体で1.5〜2秒以内に収めるのが目安です。
5つのボックスが順番に現れましたか? 各ボックスの間隔は0.08秒。ほんのわずかな差ですが、「一斉に現れる」のとは印象がまったく違いますよね。
スクロール連動アニメーション — 要素が画面に入ったら動かす
ヒーロー以外のセクションにも、スクロールに合わせて要素が現れるアニメーションを加えてみましょう。ページを下にスクロールしていくと、コンセプトやメニューのセクションがふわっと登場する、あの演出です。
仕組みを理解する
スクロール連動アニメーションは、大きく分けて2つのアプローチがあります。
ひとつは、JavaScriptの Intersection Observer API を使う方法。要素が画面内に入ったタイミングを検知して、CSSクラスを付与する仕組みです。番外編②のJavaScript回で実装するので、今回はCSSの準備だけしておきます。
もうひとつは、CSSの animation-timeline を使う方法。JavaScriptなしでスクロール連動アニメーションが実現できる新しい仕様です。Chrome、Edge、Safariではすでに使えますが、Firefoxではまだ標準で有効になっていないため、今の段階ではすべてのブラウザで確実に動くIntersection Observerのアプローチを採用します。
CSSの準備 — 「待機状態」と「登場状態」を作る
考え方はシンプルです。最初は要素を透明にしておき、画面に入ったときにクラスを追加して、アニメーションを再生する。そのために、まず「まだ見えていない状態」の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属性に追記してください。
<!-- コンセプトセクション -->
<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 を追加するだけです。
この時点でブラウザを確認すると、fade-in-up を追加した要素はすべて透明で見えなくなっているはずです。is-visible クラスが付いていないので、待機状態のままだからですね。
JavaScriptで is-visible クラスを付与する処理は、番外編②で実装します。今すぐ動作を確認したい方は、ブラウザのデベロッパーツールで要素を選んで手動で is-visible クラスを追加してみてください。ふわっと現れるはずです。
<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 のスタイルを振り返ってみましょう。
.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);
}
ここに、より繊細な影の変化を加えてみましょう。通常時にもごく薄い影を入れておくと、ホバー時の変化がより滑らかに見えます。
/* .menu-card の既存スタイルに追加 */
.menu-card {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
/* ↑ この1行を追加。通常時のごく薄い影 */
}
通常時の box-shadow がゼロだと、ホバーしたときに「影が突然現れる」ように見えることがあります。最初からごく薄い影をつけておくと、変化がなめらかになります。
カーソルを載せてみてください
カードがふわっと浮き上がり、内側の画像がわずかにズームする。この2つの動きが同時に起きることで、カードが「生きている」ような印象を与えます。
cubic-bezier を変えるだけで印象が変わる
第7回では var(--transition-base)(0.3s ease)を使っていましたが、cubic-bezier を変えるだけで動きの印象は大きく変わります。
/* :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本の横棒(バー)でできています。
<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本の線=×印が現れます。
.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本分の間隔ぶん移動する」という計算です。この値は、ご自身のバーの太さや間隔に合わせて調整してくださいね。
クリックして切り替え
クリックするたびに、三本線と×印がスムーズに切り替わりましたか? transition が付いているおかげで、transform の変化がアニメーションとして表現されます。瞬間的に変わるのではなく、バーが「動いて」×になる。これだけで、UIの品質が一段上がります。
prefers-reduced-motion — 動きが苦手な人への配慮
ここまでアニメーションを追加してきましたが、忘れてはいけないことがあります。すべてのユーザーがアニメーションを心地よいと感じるわけではない、ということです。
前庭障害(めまいや乗り物酔いを起こしやすい疾患)のある方にとって、画面の動きは吐き気や頭痛の原因になることがあります。また、注意欠如の傾向がある方には、動きが集中の妨げになる場合もあります。
そのために用意されているのが、prefers-reduced-motion というメディアクエリです。ユーザーのデバイス設定で「視覚効果を減らす」がオンになっている場合、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 の正しい使い方です。
アクセシビリティは「おまけ」ではなく、設計に最初から組み込むもの。アニメーションを加えるときは、同時にそれを止める手段も用意する。
パフォーマンスを意識する
最後に、アニメーションのパフォーマンスについて少し触れておきます。動きが「カクつく」のは、技術的に何が起きているかを知っておくと、自分でトラブルシューティングできるようになります。
なぜ transform と opacity だけを推奨するのか
ブラウザが画面を表示するまでには、大きく分けて「レイアウト → ペイント → コンポジット」という3つのステップがあります。
width や margin を変化させると、最初の「レイアウト」からやり直しになります。ページ上の他の要素の位置も再計算されるので、処理が重くなります。
background-color や box-shadow の変化は「ペイント」のやり直し。レイアウトよりは軽いですが、それでも負荷があります。
一方、transform と opacity は「コンポジット」の段階だけで完結します。GPUが効率的に処理してくれるので、60fps(1秒間に60コマ)の滑らかな動きが実現できるのです。
will-change は「ここぞ」のときだけ
will-change というCSSプロパティがあります。「この要素はこのプロパティが変化しますよ」とブラウザに事前に伝えることで、最適化の準備をしてもらう仕組みです。
.menu-card {
will-change: transform;
}
ただし、これはあくまで「最終手段」です。ブラウザは要素ごとにメモリを確保するため、たくさんの要素に will-change を付けると逆にパフォーマンスが悪化します。「特定のアニメーションがカクつく」と感じたときだけ、ピンポイントで使ってください。
今回のcafé SOELサイトでは、transform と opacity だけをアニメーションさせているので、will-change を明示的に書く必要はほぼありません。ブラウザが自動で最適化してくれます。
今回追加したコードのまとめ
この番外編で 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 では以下の変更を行いました。
<!-- 以下の要素に 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によるクラス付与」で実現する
- アニメーションで動かすプロパティは transform と opacity に絞ると、60fpsの滑らかな動きが得られる
- prefers-reduced-motion メディアクエリで、動きが苦手なユーザーへの配慮を忘れない
- will-change は「カクつきが出たときの最終手段」。むやみに使わない
- よいアニメーションは「気づかないくらい自然」で、「短く・控えめ」であること
ここまでで、CSSだけで実現できるアニメーションの基礎はすべてカバーしました。次の番外編②では、JavaScriptを使ってハンバーガーメニューの開閉やスムーススクロールなど、インタラクション(操作への反応)を実装していきます。今回準備したスクロールアニメーションも、番外編②で仕組みを詳しく解説しますので、楽しみにしていてくださいね。