CSSアニメーション入門:transition / transform / keyframesを“動かして学ぶ”

CSSアニメーションは、JavaScriptなしでも 「気持ちいいUI」 を作れる武器です。
この記事では、まず transition(ホバーなどの滑らか変化) を押さえ、次に transform(移動・拡大縮小・回転)、そして @keyframes(繰り返し・複雑な動き) までを、最後に ミニUIを作るハンズオン で体に入れます。


この記事でできるようになること

  • transition と animation の違いがわかる
  • transform を使った“軽い”動きが作れる
  • keyframes でローディングやバッジの動きを作れる
  • パフォーマンスとアクセシビリティ(prefers-reduced-motion)を理解する
  • コピペで動く「アニメーション練習ページ」を作れる

1. CSSアニメーションの全体像(まずここだけで8割)

transition:状態変化を滑らかにする

  • 例:hoverで色が変わる、ボタンが少し浮く
  • トリガー(状態変化)が必要(hover/focus/クラス付け替え等)

animation(@keyframes):状態変化なしで勝手に動ける

  • 例:ローディング、点滅、バウンド、無限回転
  • 時間経過で勝手に進む

2. transition の基本(ホバーを“それっぽく”する)

最小構成:

.button {
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 30px rgba(0,0,0,.15);
}

ポイント:

  • transitionhover側ではなく通常側 に書く(戻る時も滑らかになる)
  • まずは transformopacity を優先して使う(軽い)

よく使う easing(動きのクセ)

  • ease:無難
  • ease-in:最初遅く、後半速い
  • ease-out:最初速く、最後ゆっくり(UIに合うこと多い)
  • cubic-bezier(...):職人芸(必要になってからでOK)

3. transform:軽くて強い(移動・拡大・回転)

CSSで動かすならまずこれ。

.card:hover {
  transform: translateY(-6px) scale(1.02);
}

transformの代表:

  • translateX/Y():移動
  • scale():拡大縮小
  • rotate():回転
  • skew():歪み(使い所は選ぶ)

注意:レイアウトを変える top/left/width/height のアニメは重くなりがち。
可能なら transform で代替すると滑らかです。


4. keyframes:ローディングや“勝手に動く”UI

例:回転するローダー

.spinner {
  animation: spin 900ms linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

アニメーション指定の基本:

animation: name duration timing-function delay iteration-count direction fill-mode;

よく使うのはこのへん:

  • infinite(無限)
  • alternate(行ったり来たり)
  • forwards(終点で止める)

5. パフォーマンスの基本:動かすなら「opacity / transform」

滑らかさ(フレーム落ち)を避けるコツはシンプル。

  • できるだけ transform / opacity を動かす
  • width/height/top/left は避ける(レイアウト計算が増えやすい)
  • 影(box-shadow)も重いことがあるので “控えめ” に

必要に応じて:

.will-animate {
  will-change: transform;
}

will-change は乱用すると逆効果なので「ここぞ」にだけ。


6. アクセシビリティ:動きが苦手な人への配慮(必須級)

OS設定で「動きを減らす」をONにしている人がいます。
prefers-reduced-motion に対応すると、ちゃんとして見えます。

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

ハンズオン:アニメーション練習ページを作る(コピペで動く)

以下を作ります:

  • ① ホバーで浮くボタン(transition)
  • ② カードがふわっと浮く(transform)
  • ③ ローディング(keyframes)
  • ④ トースト風通知(登場→停止)(fill-mode)
  • ⑤ スケルトンローディング(シマー)

1) フォルダ構成

css-animation-hands-on/
  index.html
  styles.css

2) index.html(コピペOK)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>CSSアニメーション入門ハンズオン</title>
  <link rel="stylesheet" href="styles.css" />
</head>
<body>
  <header class="header">
    <div class="container">
      <h1>CSSアニメーション入門</h1>
      <p class="muted">transition / transform / keyframes をまとめて練習します。</p>
    </div>
  </header>

  <main class="container">
    <section class="section">
      <h2>1) ボタン:ホバーで浮く(transition)</h2>
      <div class="row">
        <a class="btn" href="#">Primary</a>
        <a class="btn btn--ghost" href="#">Ghost</a>
      </div>
      <p class="hint">ポイント:transition は通常状態(.btn)に書く。</p>
    </section>

    <section class="section">
      <h2>2) カード:ふわっと持ち上がる(transform)</h2>
      <div class="cards">
        <article class="card">
          <h3>Card A</h3>
          <p>translate と scale で軽く“反応”させる。</p>
        </article>
        <article class="card">
          <h3>Card B</h3>
          <p>影は控えめに。やりすぎると重くなることも。</p>
        </article>
        <article class="card">
          <h3>Card C</h3>
          <p>UIは「気配」くらいがちょうどいい。</p>
        </article>
      </div>
    </section>

    <section class="section">
      <h2>3) ローディング:回転(@keyframes)</h2>
      <div class="row">
        <div class="spinner" aria-label="loading"></div>
        <div class="dots" aria-label="loading dots">
          <span></span><span></span><span></span>
        </div>
      </div>
    </section>

    <section class="section">
      <h2>4) トースト風:登場して止まる(fill-mode: forwards)</h2>
      <div class="toast" role="status">
        ✅ 保存しました(ダミー)
      </div>
      <p class="hint">ポイント:forwards で「終点で止める」。</p>
    </section>

    <section class="section">
      <h2>5) スケルトン:シマー(shimmer)</h2>
      <div class="skeleton">
        <div class="sk-title"></div>
        <div class="sk-line"></div>
        <div class="sk-line"></div>
        <div class="sk-line short"></div>
      </div>
    </section>
  </main>

  <footer class="footer">
    <div class="container muted">© CSS Animation Hands-on</div>
  </footer>
</body>
</html>

3) styles.css(コピペOK)

/* ---------------------------------------
  0) ベース
---------------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

:root{
  --bg: #0b1020;
  --surface: rgba(255,255,255,0.06);
  --border: rgba(255,255,255,0.12);
  --text: #eaf0ff;
  --muted: rgba(234,240,255,0.75);
  --primary: #4f7cff;
  --primary2: #8aa6ff;
  --radius: 16px;
  --shadow: 0 18px 50px rgba(0,0,0,0.35);
}

body{
  background: radial-gradient(1000px 400px at 25% 0%, rgba(79,124,255,0.25), transparent),
              radial-gradient(900px 400px at 80% 10%, rgba(138,166,255,0.18), transparent),
              var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", sans-serif;
  line-height: 1.7;
}

.container{
  max-width: 980px;
  margin: 0 auto;
  padding: 0 16px;
}

.header{
  padding: 28px 0 10px;
  border-bottom: 1px solid var(--border);
}

.footer{
  padding: 24px 0 40px;
  border-top: 1px solid var(--border);
  margin-top: 40px;
}

h1{ margin: 0 0 4px; font-size: 34px; letter-spacing: -0.02em; }
h2{ margin: 0 0 10px; font-size: 22px; }
h3{ margin: 0 0 6px; }

.muted{ color: var(--muted); }
.section{ padding: 22px 0; }

.row{
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}

.hint{
  margin: 10px 0 0;
  color: rgba(234,240,255,0.6);
  font-size: 13px;
}

/* ---------------------------------------
  1) transition:ボタン
---------------------------------------- */
.btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 14px;
  border-radius: 12px;
  color: #07102a;
  font-weight: 800;
  text-decoration: none;
  background: linear-gradient(135deg, var(--primary), var(--primary2));
  border: 1px solid transparent;
  box-shadow: 0 10px 24px rgba(79,124,255,0.25);

  /* ✅ 通常状態に書く:戻る時も滑らか */
  transition: transform 160ms ease-out, box-shadow 160ms ease-out, filter 160ms ease-out;
}

.btn:hover{
  transform: translateY(-2px);
  filter: brightness(1.05);
  box-shadow: 0 16px 36px rgba(79,124,255,0.32);
}

.btn:active{
  transform: translateY(0px) scale(0.99);
}

.btn--ghost{
  background: transparent;
  color: var(--text);
  border-color: var(--border);
  box-shadow: none;
}

.btn--ghost:hover{
  box-shadow: none;
  background: rgba(255,255,255,0.06);
}

/* ---------------------------------------
  2) transform:カード
---------------------------------------- */
.cards{
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}

.card{
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: 0 0 0 rgba(0,0,0,0);

  transition: transform 180ms ease-out, box-shadow 180ms ease-out;
}

.card:hover{
  transform: translateY(-6px) scale(1.01);
  box-shadow: var(--shadow);
}

/* ---------------------------------------
  3) keyframes:スピナー
---------------------------------------- */
.spinner{
  width: 34px;
  height: 34px;
  border-radius: 999px;
  border: 3px solid rgba(234,240,255,0.22);
  border-top-color: rgba(234,240,255,0.95);
  animation: spin 900ms linear infinite;
}

@keyframes spin{
  to { transform: rotate(360deg); }
}

/* dots(点が跳ねる) */
.dots{
  display: inline-flex;
  gap: 6px;
  align-items: center;
}

.dots span{
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: rgba(234,240,255,0.85);
  opacity: 0.35;
  transform: translateY(0);
  animation: bounce 700ms ease-in-out infinite;
}

.dots span:nth-child(2){ animation-delay: 120ms; }
.dots span:nth-child(3){ animation-delay: 240ms; }

@keyframes bounce{
  0%, 100% { transform: translateY(0); opacity: 0.35; }
  50%      { transform: translateY(-6px); opacity: 1; }
}

/* ---------------------------------------
  4) トースト:登場して止まる
---------------------------------------- */
.toast{
  width: fit-content;
  max-width: 100%;
  padding: 10px 12px;
  border-radius: 12px;
  background: rgba(79,124,255,0.14);
  border: 1px solid rgba(79,124,255,0.35);
  color: var(--text);

  /* 最初はちょい下&透明(ただし見せたいのでJS不要の自動登場) */
  transform: translateY(8px);
  opacity: 0;

  animation: toast-in 700ms ease-out 200ms forwards;
}

@keyframes toast-in{
  to { transform: translateY(0); opacity: 1; }
}

/* ---------------------------------------
  5) スケルトン:シマー
---------------------------------------- */
.skeleton{
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 16px;
  width: min(560px, 100%);
  overflow: hidden;
  position: relative;
}

.sk-title, .sk-line{
  border-radius: 10px;
  background: rgba(234,240,255,0.10);
  position: relative;
}

.sk-title{ height: 18px; width: 52%; margin-bottom: 12px; }
.sk-line{ height: 12px; width: 100%; margin-bottom: 10px; }
.sk-line.short{ width: 68%; margin-bottom: 0; }

/* シマーの光 */
.skeleton::before{
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-40%);
  background: linear-gradient(
    90deg,
    transparent,
    rgba(234,240,255,0.10),
    transparent
  );
  animation: shimmer 1200ms ease-in-out infinite;
}

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

/* ---------------------------------------
  6) レスポンシブ
---------------------------------------- */
@media (max-width: 860px){
  .cards{ grid-template-columns: 1fr; }
}

/* ---------------------------------------
  7) アクセシビリティ:動きを減らす設定に対応
---------------------------------------- */
@media (prefers-reduced-motion: reduce){
  *{
    animation: none !important;
    transition: none !important;
  }
}

4) 触って理解する「練習メニュー」

このページが表示できたら、次の順に編集して“体で覚える”のがおすすめです。

A. ボタンの動きを調整してみる

  • translateY(-2px)-4px にしてみる
  • duration160ms → 300ms にして“もっさり”を確認する
  • ease-outlinear にして不自然さを体感する

B. カードに「傾き」を足してみる(軽く)

card:hover に追加:

transform: translateY(-6px) scale(1.01) rotate(-0.4deg);

C. スピナーを速く/遅く

animation: spin 400ms linear infinite;

または 1400ms にして比較。

D. トーストを「一度表示して消える」にする

toast-in を2段階に(表示→少し待つ→薄くなる)

@keyframes toast-in{
  0%   { transform: translateY(8px); opacity: 0; }
  20%  { transform: translateY(0); opacity: 1; }
  80%  { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(-2px); opacity: 0; }
}

そして .toastanimation を少し長く:

animation: toast-in 2600ms ease-out 200ms forwards;

よくある落とし穴(ここだけ読めば回避できる)

1) transition を hover 側に書いて戻りがカクつく

✅ 通常状態に書く(この記事のボタンがそれ)

2) 動かすプロパティが重い

  • top/left/width/height をアニメするとカクつきやすい
    transform で代用する

3) 動きを盛りすぎて“安っぽい”

  • UIは「気配」で十分
    → 150〜220ms くらい、移動は 2〜8px が扱いやすい

4) reduced motion 未対応

  • ちゃんとしたサービスほど対応してる
    → 最低でも prefers-reduced-motion を入れる


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です