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);
}
ポイント:
transitionは hover側ではなく通常側 に書く(戻る時も滑らかになる)- まずは
transformとopacityを優先して使う(軽い)
よく使う 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にしてみるdurationを160ms → 300msにして“もっさり”を確認するease-outをlinearにして不自然さを体感する
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; }
}
そして .toast の animation を少し長く:
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を入れる
コメントを残す