



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒画像の読み込みとか通信待ちのときに、くるくる回るローディングを出したいんです。CSSだけで作れますか?20種類くらい欲しくて、コピペで使えると助かるんですけど…。
ペン博士いいテーマだね!ローディングアニメーションはCSSの@keyframesだけでほとんど作れるよ。JavaScriptもプラグインもいらない。この記事では円スピナー・ドット・バー・プログレスバー・パルス・スケルトン・波・円形プログレスまで、コピペで即使える20種類を、受け皿のHTMLと動くCSSをセットで全部見せていくね。仕組みも分かるように@keyframesの基礎から解説するよ!
Webサイトやアプリで、データの読み込み中や通信待ちのあいだに「くるくる回るアイコン」や「点滅するドット」を見たことがあるはずです。これがローディングアニメーション(ローディングインジケーター)です。処理に時間がかかっているとき、画面が真っ白のままだとユーザーは「固まった?」と不安になります。そこで「いま処理中ですよ」と伝えるのがローディングの役割です。
この記事では、CSSだけで作れるローディングアニメーションを20種類、コピペですぐ使える実装サンプルとしてまとめました。円スピナー・二重リング・ドット3つ・バー往復・プログレスバー・パルス・スケルトンスクリーン・波・円形プログレスなど、実務でよく使うパターンを網羅しています。すべて外部ライブラリ不要、受け皿のHTMLと動くCSS(@keyframesを含む)をセットで掲載しているので、丸ごとコピーすればそのまま動きます。
あわせて、アニメーションの土台となる@keyframesとanimationプロパティの基礎、モーションが苦手な人に配慮するprefers-reduced-motion、カクつきを防ぐパフォーマンスのコツ、JavaScriptでローディングを表示・非表示に切り替える方法、読み上げソフトに配慮するaria-busyやrole="status"まで、初心者の方がつまずきやすいポイントを丁寧に解説します。最後まで読めば、シーンに合わせて自分でローディングを選び、調整して使えるようになります。
先に結論からお伝えします。ローディングアニメーションは、特別なライブラリを入れなくてもCSSの@keyframesとanimationプロパティだけでほとんど作れます。押さえるべきポイントは次のとおりです。
この記事の20サンプルは、すべてこの原理の応用です。まずは全体像として「どんな種類があるのか」を一覧で確認し、それから@keyframesの基礎、そして20種類の実装サンプルへと進んでいきましょう。気に入ったものをコピーして、色やサイズの数字を変えるだけで自分のサイトに使えます。
最初に、この記事で紹介する20種類を見た目と使いどころで一覧にまとめました。「どんな場面でどれを使えばいいか」の目安にしてください。名前をクリックできる目次からも各サンプルへ飛べます。
| # | 種類 | 動きの仕組み | 使いどころの目安 |
|---|---|---|---|
| 1 | 円スピナー | border+rotate | 最も定番。通信待ち全般 |
| 2 | 二重リング | 2つのリングを逆回転 | 少しリッチに見せたいとき |
| 3 | ドット点滅(3つ) | opacityを時間差で | テキストの近く・省スペース |
| 4 | ドットバウンド | translateYで跳ねる | 軽快な印象を出したいとき |
| 5 | バー往復 | translateXで左右移動 | 横長スペース・ヘッダー下 |
| 6 | プログレスバー(不定) | 背景を左右にスライド | 進捗が読めない読み込み |
| 7 | プログレスバー(確定) | widthをJSで更新 | %が分かるダウンロード等 |
| 8 | パルス(拡大縮小) | scaleとopacity | やわらかく主張しすぎない |
| 9 | パルスリング(波紋) | scale+opacityで拡散 | 地図ピン・通知の強調 |
| 10 | スケルトンスクリーン | linear-gradientのシマー | 記事・カード一覧の読み込み |
| 11 | 回転四角 | rotateで四角が回る | シンプルで軽量 |
| 12 | 反転する四角 | rotateX/rotateY | 立体感のあるアクセント |
| 13 | 波(バー3〜5本) | scaleYを時間差で | 音声・録音・処理中 |
| 14 | テキスト…(3点リーダー) | 疑似要素とcontent切替 | 「読み込み中…」の文末 |
| 15 | 円形プログレス | conic-gradient | 円グラフ状の進捗 |
| 16 | 回転グラデーション | conic-gradient+rotate | モダンな円スピナー |
| 17 | 跳ねるボール(影付き) | translateYと影のscale | 遊び心のある演出 |
| 18 | フリップカード読込 | rotateYで裏返る | カードUIの読み込み |
| 19 | 円周をなぞる点 | 回転する親要素 | 軌道を描くスピナー |
| 20 | オーバーレイ全画面 | 半透明背景+中央配置 | ページ全体の読み込み |
表を見ると、動きの仕組みは「回転」「点滅(opacity)」「移動(translate)」「伸縮(scale)」の組み合わせがほとんどだと分かります。裏を返せば、この4つの動かし方を覚えれば、あとは応用でいくらでも作れるということです。次の章で、その土台となる@keyframesとanimationを解説します。
サンプルに入る前に、すべての土台となる@keyframes(キーフレーム)とanimationプロパティを理解しておきましょう。ここが分かると、20種類のコードがすべて「同じ仕組みの応用」として読めるようになります。逆にここを飛ばすと、コピペはできても調整ができません。
@keyframesは、アニメーションの動きの台本(どの時点でどんな状態か)を書くための仕組みです。0%(開始)から100%(終了)までの間に、要素をどう変化させるかを指定します。次のコードは「0%で回転0度、100%で回転360度」という台本です。
/* rotate という名前の台本を定義 */
@keyframes rotate {
0% { transform: rotate(0deg); } /* 開始:0度 */
100% { transform: rotate(360deg); } /* 終了:360度=1回転 */
}
/* from / to でも書ける(0% / 100% と同じ意味) */
@keyframes rotate2 {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
台本を定義しただけでは何も動きません。この台本をどの要素で・何秒かけて・何回再生するかを指定して、初めてアニメーションが動きます。それを担うのがanimationプロパティです。
animationは、定義した@keyframesを要素に適用するためのプロパティです。実は複数のプロパティをまとめた「一括指定(ショートハンド)」で、次の8つを一行で書けます。
| プロパティ | 意味 | よく使う値 |
|---|---|---|
| animation-name | 再生する@keyframesの名前 | rotate など |
| animation-duration | 1回にかかる時間 | 1s / 0.8s / 1.5s |
| animation-timing-function | 速度の変化(イージング) | linear / ease / ease-in-out |
| animation-delay | 開始までの遅延 | 0s / 0.2s |
| animation-iteration-count | 繰り返す回数 | infinite / 3 |
| animation-direction | 再生方向 | normal / alternate |
| animation-fill-mode | 再生前後の状態 | forwards / none |
| animation-play-state | 再生/一時停止 | running / paused |
.spinner {
/* 一括指定(ショートハンド)の書き方 */
/* 名前 時間 イージング 遅延 回数 方向 */
animation: rotate 1s linear 0s infinite normal;
}
/* 分けて書くと意味が分かりやすい */
.spinner2 {
animation-name: rotate; /* rotate 台本を使う */
animation-duration: 1s; /* 1秒で1周 */
animation-timing-function: linear; /* 一定速度で */
animation-iteration-count: infinite; /* 無限に繰り返す */
}
ローディングで特に重要なのがanimation-iteration-countにinfiniteを指定することです。ローディングは「あと何秒で終わるか分からない待ち時間」に出すものなので、決まった回数ではなくinfinite(無限)で回し続けるのが基本です。逆に、1回だけ動かしたい演出なら回数を数字で指定します。
同じ台本でも、animation-timing-function(イージング)を変えると印象が大きく変わります。これは時間の進み方(速度の緩急)を指定するものです。ローディングでよく使うものを覚えておきましょう。
たとえばスピナーの回転にease-in-outを使うと、回転が一定にならずガクガクして見えることがあります。回転系はlinear、伸縮・往復系はease-in-outと覚えておくと失敗が減ります。
animation-direction: alternate;を指定すると、台本を行って戻る(往復)ように再生してくれます。バーの左右移動やパルスの拡大縮小で、わざわざ台本に戻りの動きを書かなくても、これ1つで自然な往復になります。
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
.bar {
/* alternate なので 0→100px→0→100px… と往復する */
animation: slide 0.8s ease-in-out infinite alternate;
}
ここまでが土台知識です。@keyframesで台本を書き、animationで再生し、infiniteで回し続け、timing-functionとdirectionで質感を整える。これがローディングアニメーションのすべてです。それでは、いよいよ20種類の実装サンプルに入りましょう。すべて受け皿のHTMLと動くCSSをセットで載せています。
最初は、ローディングといえばこれ、という円スピナーです。円の一部だけ色を変えたリングを回転させることで、くるくる回っているように見せます。仕組みはとてもシンプルで、borderの一辺だけ色を変え、rotateで回すだけです。
受け皿のHTMLはdiv1つだけ。読み上げソフトのためにrole="status"とaria-labelを付けておくのがおすすめです(詳しくは後半のアクセシビリティの章で解説します)。
<div class="spinner" role="status" aria-label="読み込み中"></div>
.spinner {
width: 48px;
height: 48px;
border: 5px solid #e0e0e0; /* 土台のリング(薄いグレー) */
border-top-color: #3b82f6; /* 上だけ青くする=回転が見える */
border-radius: 50%; /* 正円にする */
animation: spinner-rotate 0.9s linear infinite;
}
@keyframes spinner-rotate {
to { transform: rotate(360deg); } /* 1周まわす */
}
ポイントはborder全体を薄いグレーにして、border-top-colorだけ濃い色にすること。こうすると「濃い部分が回っている」ように見えます。色を変えればブランドカラーに合わせられますし、width・height・borderの太さを変えればサイズ調整も自由自在です。まずはこの1つを完全に理解すれば、残りの応用がぐっと楽になります。
次は、円スピナーを少し発展させた二重リングです。外側のリングと内側のリングを逆方向に回転させることで、単純なスピナーよりリッチで動きのある印象になります。子要素(疑似要素)を使って2つのリングを重ねます。
<div class="dual-ring" role="status" aria-label="読み込み中"></div>
.dual-ring {
position: relative;
width: 56px;
height: 56px;
border: 5px solid transparent;
border-top-color: #3b82f6; /* 外リングは青 */
border-radius: 50%;
animation: dual-rotate 1s linear infinite;
}
.dual-ring::after {
content: "";
position: absolute;
inset: 6px; /* 内側に一回り小さく配置 */
border: 5px solid transparent;
border-top-color: #f97316; /* 内リングはオレンジ */
border-radius: 50%;
/* reverse で逆回転させる */
animation: dual-rotate 0.7s linear infinite reverse;
}
@keyframes dual-rotate {
to { transform: rotate(360deg); }
}
鍵になるのは::afterに animation … reverse を付けて逆回転させる点と、外と内でduration(回転速度)を変えている点です。速度をあえてずらすことで、単調にならず動きに奥行きが出ます。insetで内側リングの位置を、border-colorで色をそれぞれ調整できます。
小さなスペースに収めたいときに便利なのが3つのドットが順番に点滅するローディングです。「読み込み中…」のようなテキストのそばに置いても違和感がありません。3つの丸を並べ、opacityを時間差(animation-delay)で点滅させます。
<div class="dots" role="status" aria-label="読み込み中">
<span></span>
<span></span>
<span></span>
</div>
.dots {
display: flex;
gap: 8px;
}
.dots span {
width: 12px;
height: 12px;
border-radius: 50%;
background: #3b82f6;
animation: dot-blink 1.2s ease-in-out infinite;
}
/* 2つ目・3つ目に遅延を付けて順番に光らせる */
.dots span:nth-child(2) { animation-delay: 0.2s; }
.dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes dot-blink {
0%, 100% { opacity: 0.2; } /* 消えぎわ */
50% { opacity: 1; } /* 明るく */
}
ポイントはnth-child()で2つ目・3つ目にanimation-delayをずらして設定すること。同じ台本でも開始タイミングをずらすだけで、「光が流れていく」ように見えます。この「delayで時間差をつける」テクニックは、次のバウンドや波でも繰り返し登場する超重要パターンです。
同じ3つのドットでも、点滅ではなく上下に跳ねさせる(バウンド)と、より軽快でかわいらしい印象になります。translateYで上下移動させ、こちらもanimation-delayで時間差をつけます。HTMLはサンプル3と同じ構造です。
<div class="bounce" role="status" aria-label="読み込み中">
<span></span>
<span></span>
<span></span>
</div>
.bounce {
display: flex;
gap: 8px;
}
.bounce span {
width: 12px;
height: 12px;
border-radius: 50%;
background: #10b981;
animation: dot-bounce 0.6s ease-in-out infinite alternate;
}
.bounce span:nth-child(2) { animation-delay: 0.15s; }
.bounce span:nth-child(3) { animation-delay: 0.3s; }
@keyframes dot-bounce {
from { transform: translateY(0); }
to { transform: translateY(-14px); } /* 上に跳ねる */
}
ここではalternateで「上がって戻る」往復を自動化しています。台本には「上に上がる」動きだけ書き、戻りはalternateに任せているのがスマートな点です。ease-in-outを使うことで、頂点でふわっと減速して自然な跳ね方になります。translateYの数値を大きくすれば、より大きく跳ねます。
ヘッダー下やカードの上端など横に長いスペースには、小さなバーが左右に往復するローディングが似合います。translateXで左右に動かします。親要素にoverflow: hidden;を付けて、はみ出しを隠すのがコツです。
<div class="track" role="status" aria-label="読み込み中">
<div class="thumb"></div>
</div>
.track {
position: relative;
width: 200px;
height: 6px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden; /* バーのはみ出しを隠す */
}
.thumb {
position: absolute;
left: 0;
width: 60px;
height: 100%;
background: #3b82f6;
border-radius: 999px;
animation: bar-move 1.1s ease-in-out infinite alternate;
}
@keyframes bar-move {
from { transform: translateX(0); }
to { transform: translateX(140px); } /* 親幅200 − バー幅60 = 140 */
}
移動量は「親の幅 − バーの幅」ぴったりにすると、端まで行ってきれいに折り返します(この例では200−60=140px)。移動にはleftではなくtransform: translateX()を使っている点に注目してください。transformはGPUで処理されて軽く、滑らかに動きます(理由は後半のパフォーマンスの章で解説します)。
「進捗の%は分からないけれど、動いていることは伝えたい」――そんなときに使うのが不定(indeterminate)プログレスバーです。背景のグラデーションを左から右へ流し続けることで、絶えず動いている印象を与えます。background-positionをアニメーションさせるのがポイントです。
<div class="indeterminate" role="status" aria-label="読み込み中"></div>
.indeterminate {
width: 100%;
max-width: 320px;
height: 8px;
border-radius: 999px;
/* 動く帯を作るためのグラデーション */
background: linear-gradient(90deg, #e5e7eb 25%, #3b82f6 50%, #e5e7eb 75%);
background-size: 200% 100%; /* 幅を倍にして流せるようにする */
animation: indeterminate 1.2s linear infinite;
}
@keyframes indeterminate {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
コツはbackground-sizeを200%にして、background-positionを動かすこと。実際の要素は動かさず、背景の柄だけを流すことで軽い動きになります。次の確定プログレスバーと見た目が似ていますが、こちらは「%が決まっていない」場面用だと覚えておきましょう。
ファイルのダウンロードやアップロードのように進捗の%が分かる場合は、確定プログレスバーを使います。バーのwidthを進捗に合わせて伸ばします。%の更新自体はJavaScriptで行うので、CSSは「伸び方を滑らかにする」ことに専念します。
<div class="progress">
<div class="progress-fill" style="width: 0%;"></div>
</div>
<p class="progress-label">0%</p>
.progress {
width: 100%;
max-width: 320px;
height: 10px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #3b82f6;
border-radius: 999px;
/* width が変わったとき 0.3 秒かけて滑らかに伸ばす */
transition: width 0.3s ease;
}
ここでは@keyframesではなくtransitionを使う点に注目してください。進捗は「0→30→70→100」のように外から与えられた値へ変化するので、繰り返しのanimationではなく、値の変化を滑らかにするtransitionが向いています。実際に%を更新するJavaScriptは次のとおりです。
const fill = document.querySelector(".progress-fill");
const label = document.querySelector(".progress-label");
function setProgress(percent) {
const value = Math.min(100, Math.max(0, percent)); // 0〜100に丸める
fill.style.width = value + "%";
label.textContent = value + "%";
}
// 例:擬似的に進捗を進めるデモ
let current = 0;
const timer = setInterval(() => {
current += 10;
setProgress(current);
if (current >= 100) clearInterval(timer); // 100%で停止
}, 400);
実務では、このsetProgress()をファイル読み込みの進捗イベント(progressイベント)などから呼び出します。transitionのおかげで、値が飛んでもバーはガクッとせず滑らかに追従します。
主張が強すぎないローディングが欲しいときはパルス(脈打つように拡大縮小する)が向いています。丸をscale()で少し大きくしながらopacityを下げると、ふわっと呼吸するような動きになります。
<div class="pulse" role="status" aria-label="読み込み中"></div>
.pulse {
width: 48px;
height: 48px;
border-radius: 50%;
background: #3b82f6;
animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
0% { transform: scale(0.6); opacity: 1; }
100% { transform: scale(1); opacity: 0; } /* 大きくなりつつ消える */
}
scale()とopacityを同じ台本の中で同時に変化させているのがポイントです。「大きくなりながら薄くなる」ことで、光が広がって消えていくような柔らかさが出ます。この2つはどちらもGPUで処理される軽いプロパティなので、カクつきにくいのも利点です。
地図のピンや通知アイコンの周りで見かける波紋のように広がるリングも、パルスの応用です。中心の点の外側に、scaleで拡大しながらopacityで消えていくリングを重ねます。
<div class="ripple" role="status" aria-label="読み込み中">
<span class="core"></span>
<span class="wave"></span>
</div>
.ripple {
position: relative;
width: 40px;
height: 40px;
}
.ripple .core {
position: absolute;
inset: 12px;
background: #ef4444;
border-radius: 50%;
}
.ripple .wave {
position: absolute;
inset: 0;
border: 3px solid #ef4444;
border-radius: 50%;
animation: ripple 1.4s ease-out infinite;
}
@keyframes ripple {
0% { transform: scale(0.3); opacity: 1; }
100% { transform: scale(1.6); opacity: 0; } /* 広がって消える */
}
ease-outを使うと勢いよく広がって、外側でそっと消える自然な波紋になります。.waveを複数用意してanimation-delayをずらせば、波が連続して広がる表現も作れます。存在感が強いので、通知や強調したいポイントに絞って使うのがおすすめです。
近年よく使われるのがスケルトンスクリーンです。コンテンツが読み込まれる前に、記事やカードの「骨格(グレーの箱)」を先に表示し、そこに光が流れるシマー(きらめき)を入れる手法です。ユーザーは「もうすぐ表示されそう」と感じ、体感の待ち時間が短くなります。
<div class="skeleton-card">
<div class="skeleton thumb"></div>
<div class="skeleton line w80"></div>
<div class="skeleton line w60"></div>
</div>
.skeleton-card {
width: 260px;
padding: 16px;
border: 1px solid #eee;
border-radius: 12px;
}
.skeleton {
/* 骨格の下地+流れる光を重ねたグラデーション */
background: linear-gradient(90deg, #eee 25%, #f5f5f5 37%, #eee 63%);
background-size: 400% 100%;
border-radius: 6px;
animation: shimmer 1.4s ease infinite;
}
.skeleton.thumb { height: 140px; margin-bottom: 14px; }
.skeleton.line { height: 14px; margin-bottom: 10px; }
.skeleton.w80 { width: 80%; }
.skeleton.w60 { width: 60%; }
@keyframes shimmer {
from { background-position: 100% 0; }
to { background-position: 0 0; } /* 光を左へ流す */
}
仕組みは不定プログレスバーと同じでlinear-gradientの明るい帯をbackground-positionで流す(シマー)だけです。実際のコンテンツと同じ大きさ・配置で骨格を作ると、読み込み完了時の入れ替わりが自然になります。ニュース・SNS・商品一覧など、複数の要素をまとめて読み込む画面と相性抜群です。
円だけでなく四角を回転させても、立派なローディングになります。実装は円スピナーより簡単で、四角い要素をrotateで回すだけです。ミニマルなデザインのサイトによく合います。
<div class="square" role="status" aria-label="読み込み中"></div>
.square {
width: 40px;
height: 40px;
background: #3b82f6;
border-radius: 6px;
animation: square-rotate 1.2s ease-in-out infinite;
}
@keyframes square-rotate {
0% { transform: rotate(0deg) scale(1); }
50% { transform: rotate(180deg) scale(0.6); } /* 途中で小さく */
100% { transform: rotate(360deg) scale(1); }
}
ここではrotateとscaleを組み合わせて、回転しながら大小に変化させています。50%の時点で小さくすることで、単なる回転より表情が豊かになります。transformは複数の関数(rotateとscale)を空白区切りで並べて同時にかけられる、という点も覚えておくと応用が利きます。
四角を平面的にではなく立体的にパタンと反転させると、奥行きのあるアクセントになります。rotateX(横軸まわりの回転)とrotateY(縦軸まわりの回転)を交互に使い、親にperspectiveを付けて立体感を出します。
<div class="flip-wrap">
<div class="flip-box" role="status" aria-label="読み込み中"></div>
</div>
.flip-wrap {
perspective: 200px; /* 奥行き(立体感)を与える */
}
.flip-box {
width: 44px;
height: 44px;
background: #8b5cf6;
border-radius: 6px;
animation: flip 1.6s ease-in-out infinite;
}
@keyframes flip {
0% { transform: rotateX(0) rotateY(0); }
50% { transform: rotateX(180deg) rotateY(0); } /* 前後にパタン */
100% { transform: rotateX(180deg) rotateY(180deg); } /* 左右にパタン */
}
立体的な回転には親要素のperspectiveが必須です。これがないと、平面的に潰れて見えてしまいます。perspectiveの数値が小さいほど奥行きが強調され、大きいほど控えめになります。少しリッチな読み込み演出を作りたいときに便利なテクニックです。
音楽プレイヤーや録音中の表示でおなじみのイコライザー風の波も、CSSだけで作れます。縦のバーを複数並べ、scaleYで高さを伸び縮みさせ、animation-delayで時間差をつけると、波打つように見えます。
<div class="wave" role="status" aria-label="処理中">
<span></span><span></span><span></span><span></span><span></span>
</div>
.wave {
display: flex;
align-items: center;
gap: 5px;
height: 40px;
}
.wave span {
width: 6px;
height: 100%;
background: #06b6d4;
border-radius: 3px;
transform-origin: center; /* 中心を基準に伸縮 */
animation: wave 1s ease-in-out infinite;
}
.wave span:nth-child(2) { animation-delay: 0.1s; }
.wave span:nth-child(3) { animation-delay: 0.2s; }
.wave span:nth-child(4) { animation-delay: 0.3s; }
.wave span:nth-child(5) { animation-delay: 0.4s; }
@keyframes wave {
0%, 100% { transform: scaleY(0.3); }
50% { transform: scaleY(1); } /* 真ん中で伸びる */
}
ここでもnth-child()でバーごとにdelayをずらす定番テクニックが活きています。transform-origin: center;を指定すると、バーが中心から均等に伸び縮みして自然な波になります(下端を基準にしたい場合はbottomにします)。バーの本数や色を変えるだけで印象を大きく変えられます。
「読み込み中…」のような文の末尾の3点(…)だけをアニメーションさせると、シンプルながら「処理中」の雰囲気が出ます。疑似要素::afterで点を出し、contentを切り替える方法が手軽です。
<p class="loading-text" role="status">読み込み中<span class="ellipsis"></span></p>
.ellipsis::after {
content: "";
animation: ellipsis 1.2s steps(4, end) infinite;
}
@keyframes ellipsis {
0% { content: ""; }
25% { content: "."; }
50% { content: ".."; }
75% { content: "..."; }
100% { content: ""; }
}
ポイントはsteps()を使って、なめらかではなくカクカクと段階的に切り替えること。steps(4, end)にすることで「なし→.→..→…」ときれいに1段ずつ切り替わります(linearだと文字がにじむように補間されてしまいます)。文字だけで完結するので、テキストのそばに置いても浮きません。
進捗を円グラフのように見せたいときは、conic-gradient(円錐グラデーション)が便利です。角度に応じて色を塗り分けられるので、CSS変数の値を変えるだけで「何%まで塗る」を表現できます。JavaScriptで変数を更新すれば確定進捗になります。
<div class="circle-progress" style="--value: 65;">
<span class="circle-label">65%</span>
</div>
.circle-progress {
--value: 0;
position: relative;
width: 90px;
height: 90px;
border-radius: 50%;
display: grid;
place-items: center;
/* --value % まで青、それ以降はグレーで塗る */
background:
conic-gradient(#3b82f6 calc(var(--value) * 1%), #e5e7eb 0);
}
.circle-progress::before {
content: "";
position: absolute;
width: 66px;
height: 66px;
border-radius: 50%;
background: #fff; /* 中央を白く抜いてドーナツにする */
}
.circle-label {
position: relative; /* 白い円より前に出す */
font-weight: bold;
color: #3b82f6;
}
鍵はconic-gradientで –value% まで色を塗り、::beforeの白い円で中央を抜くこと。これでドーナツ型の円形プログレスになります。JavaScriptでelement.style.setProperty("--value", 80)のように更新すれば、進捗に合わせて塗りが変化します。--valueの変化にtransitionを効かせたい場合は、@propertyで型を定義するとアニメーションできます。
サンプル1の円スピナーをグラデーションで今風にしたのがこれです。conic-gradientで色が一周ぐるっと変わるリングを作り、それを回転させます。単色スピナーよりモダンでリッチに見えます。
<div class="grad-spinner" role="status" aria-label="読み込み中"></div>
.grad-spinner {
width: 52px;
height: 52px;
border-radius: 50%;
/* 透明→青へ一周するグラデーション */
background: conic-gradient(from 0deg, transparent, #3b82f6);
/* 中央をくり抜いてリングにする(maskを使用) */
-webkit-mask: radial-gradient(farthest-side, transparent 60%, #000 61%);
mask: radial-gradient(farthest-side, transparent 60%, #000 61%);
animation: grad-rotate 0.9s linear infinite;
}
@keyframes grad-rotate {
to { transform: rotate(360deg); }
}
ポイントはmaskで中央を透明に抜いて、塗りをリング状にするテクニックです。radial-gradientのマスクで「中心60%は透明、外側は表示」とすることでドーナツになります。transparentから色へ変わるグラデーションを回すことで、しっぽを引くように滑らかに回転して見えます。
ボールが跳ねて、地面の影が伸び縮みするリアルなアニメーションです。ボールの上下運動(translateY)と、影の拡大縮小(scale)を別々の要素で連動させると、本当に弾んでいるように見えます。遊び心のあるサイトのローディングにぴったりです。
<div class="ball-wrap" role="status" aria-label="読み込み中">
<div class="ball"></div>
<div class="ball-shadow"></div>
</div>
.ball-wrap {
position: relative;
width: 40px;
height: 60px;
}
.ball {
position: absolute;
top: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #f59e0b;
animation: ball-jump 0.6s ease-in-out infinite alternate;
}
.ball-shadow {
position: absolute;
bottom: 0;
width: 28px;
height: 6px;
border-radius: 50%;
background: rgba(0,0,0,0.2);
animation: shadow-scale 0.6s ease-in-out infinite alternate;
}
@keyframes ball-jump {
from { top: 0; }
to { top: 26px; } /* 落ちる */
}
@keyframes shadow-scale {
from { transform: scale(0.5); opacity: 0.4; } /* 上にいる時は影が小さい */
to { transform: scale(1); opacity: 1; } /* 着地で影が大きい */
}
リアルさの秘密はボールが下にいるとき影を大きく、上にいるとき影を小さくする点です。ボールと影で同じduration・alternateを使い、動きを同期させています。物理現象を「影の大きさ」で表現するこの発想は、他の演出にも応用できます。
カード型UIの読み込みにはカード自体がくるっと裏返る演出が似合います。rotateYで縦軸を中心に回転させ、perspectiveで立体感を出します。サンプル12の応用で、より大きな要素に使うイメージです。
<div class="flipcard-wrap">
<div class="flipcard" role="status" aria-label="読み込み中"></div>
</div>
.flipcard-wrap {
perspective: 600px;
}
.flipcard {
width: 80px;
height: 100px;
border-radius: 10px;
background: linear-gradient(135deg, #6366f1, #a855f7);
animation: flipcard 1.8s ease-in-out infinite;
}
@keyframes flipcard {
0% { transform: rotateY(0); }
50% { transform: rotateY(180deg); } /* 裏返る */
100% { transform: rotateY(360deg); } /* 一周して表に戻る */
}
perspectiveを大きめ(600px程度)にするとパースが緩やかで上品な回転になります。プレースホルダーのカードを並べて一斉に裏返せば、コンテンツ読み込み中の待ち画面として使えます。animation-delayをカードごとにずらすと、順番に裏返る演出も作れます。
小さな点が円の軌道を描いて回るスピナーです。親を回転させ、その端に置いた点を一緒に回すことで、点が円周をなぞるように動きます。回転の中心と点の位置関係を理解すると、応用の幅が広がります。
<div class="orbit" role="status" aria-label="読み込み中">
<span class="orbit-dot"></span>
</div>
.orbit {
position: relative;
width: 50px;
height: 50px;
animation: orbit-rotate 1s linear infinite; /* 親ごと回す */
}
.orbit-dot {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: #3b82f6;
}
@keyframes orbit-rotate {
to { transform: rotate(360deg); }
}
仕組みは点を親の上端に置き、親要素そのものを回転させるだけです。親が回れば、端にくっついた点も円を描いて回ります。点を複数置いてanimation-delayを変えれば、いくつもの点が軌道上を追いかける、より凝ったスピナーになります。
最後は、ページ全体の読み込み中に画面を覆う全画面オーバーレイです。半透明の背景でページ全体を覆い、中央にスピナーを置きます。ページ表示直後や、フォーム送信中に操作をブロックしたいときに使います。中身のスピナーはサンプル1を流用しています。
<div class="overlay" id="overlay">
<div class="spinner" role="status" aria-label="読み込み中"></div>
</div>
.overlay {
position: fixed;
inset: 0; /* 画面全体を覆う */
display: flex;
align-items: center;
justify-content: center; /* スピナーを中央に */
background: rgba(255, 255, 255, 0.8); /* 半透明の白 */
z-index: 9999; /* 最前面に */
}
.overlay.is-hidden {
display: none; /* 非表示にするクラス */
}
position: fixed;とinset: 0;でスクロールしても画面全体を覆い続けるのがポイントです。z-indexを大きくして最前面に出し、display: flex;の中央寄せでスピナーを画面のど真ん中に置きます。読み込みが終わったらis-hiddenクラスを付けて隠します。この切り替えは次の章のJavaScriptで行います。
ここまでのローディングは「表示され続ける」状態でした。実際には処理が始まったら表示し、終わったら消す必要があります。この切り替えはCSSだけではできず、JavaScriptで行います。基本はclassList.add / removeでクラスを付け外しするだけです。
最もおすすめなのが、display: none;を持つクラスを付け外しする方法です。サンプル20のオーバーレイを例にします。
const overlay = document.getElementById("overlay");
// 表示する:is-hidden を外す
function showLoading() {
overlay.classList.remove("is-hidden");
}
// 非表示にする:is-hidden を付ける
function hideLoading() {
overlay.classList.add("is-hidden");
}
// 例:データ取得の前後で呼び出す
async function loadData() {
showLoading();
try {
const res = await fetch("/api/data");
const data = await res.json();
// 取得したデータで画面を更新…
} catch (e) {
console.error(e);
} finally {
hideLoading(); // 成功でも失敗でも必ず消す
}
}
重要なのはfinallyブロックで必ずhideLoading()を呼ぶことです。通信が失敗したときにhideLoading()を呼び忘れると、ローディングが表示されっぱなしになってしまいます。これは初心者が最もハマるバグなので、「消す処理は必ずfinallyに書く」と覚えておきましょう。
クラスを使わず、style.displayを直接書き換える方法もあります。手軽ですが、CSSと状態が分散して管理しにくくなるため、基本はクラス方式が推奨です。
const overlay = document.getElementById("overlay");
overlay.style.display = "flex"; // 表示
overlay.style.display = "none"; // 非表示
なぜクラス方式が良いのかというと、見た目のルールはCSS、状態の切り替えはJavaScript、と役割を分けられるからです。style.displayを直接書くと、表示するときの値(flexかblockか)をJavaScript側でも知っている必要があり、CSSを変えたときに壊れやすくなります。クラスなら「クラスを付けるか外すか」だけを考えればよく、保守が楽になります。
見た目の動きは目で見える人にしか伝わりません。目が見えない・見えにくいユーザーが使う読み上げソフト(スクリーンリーダー)にも「いま読み込み中です」と伝えるには、適切なマークアップが必要です。ここは実務で見落とされがちな重要ポイントです。
これまでのサンプルで付けていたrole="status"とaria-labelが、まさにこの役割です。スピナーは見た目こそ意味を持ちますが、HTML的には空のdivです。そのままでは読み上げソフトに無視されてしまいます。
<!-- role と aria-label で「読み込み中」だと伝える -->
<div class="spinner" role="status" aria-label="読み込み中"></div>
<!-- テキストを添える形でも良い(視覚的に隠す) -->
<div class="spinner" role="status">
<span class="visually-hidden">読み込み中です</span>
</div>
role="status"は「これは状態を知らせる領域だ」と伝える指定です。aria-labelや中のテキストで「読み込み中」という言葉を添えれば、読み上げソフトがそれを読み上げてくれます。テキストを画面に出したくない場合は、次のvisually-hiddenで見た目上だけ隠しつつ読み上げには残すテクニックを使います。
/* 目には見えないが読み上げソフトには読まれる */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
読み込み中の領域全体にはaria-busy="true"を付けると、読み上げソフトに「この部分はまだ準備中」と伝えられます。読み込みが終わったらfalseに戻します。
<!-- 読み込み中 -->
<section id="list" aria-busy="true">
<div class="spinner" role="status" aria-label="読み込み中"></div>
</section>
const list = document.getElementById("list");
list.setAttribute("aria-busy", "true"); // 読み込み開始
// …データ取得後…
list.setAttribute("aria-busy", "false"); // 読み込み完了
これらは難しく見えますが、要は「いま処理中である」という情報を、見た目だけでなく言葉でも伝えるというだけの話です。属性を1〜2個足すだけで、より多くの人にやさしいサイトになります。仕事でWeb制作をするなら、ぜひ習慣にしておきたいポイントです。
動くアニメーションは楽しい一方で、激しい動きで気分が悪くなる(乗り物酔いのような症状の)人もいます。そうしたユーザーのために、OSには「視差効果を減らす」といった設定があり、CSSからはprefers-reduced-motionで検出できます。
この設定がオンのときは、アニメーションを止める・控えめにする配慮を入れましょう。次のように、メディアクエリでアニメーションを無効化するのが基本です。
/* 「動きを減らす」設定の人にはアニメーションを止める */
@media (prefers-reduced-motion: reduce) {
.spinner,
.dots span,
.wave span {
animation: none; /* 動きを止める */
}
/* 完全に消すと状態が伝わらないので、静的な代替を見せる */
.spinner {
border-top-color: #3b82f6; /* 色は残して「処理中」を示す */
}
}
ここで気をつけたいのはアニメーションを止めても「読み込み中」という状態は伝わるようにすることです。動きを完全に消して真っ白にしてしまうと、今度は「固まった?」と不安にさせてしまいます。動きは止めても、色やテキスト(role="status")で状態が分かるようにしておくのが理想です。
なお、サイト全体でまとめて配慮したい場合は、次のようにすべてのアニメーションとトランジションをほぼ無効化する書き方もよく使われます。個別指定が漏れるのを防げます。
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
せっかくのローディングも、カクついて(カクカクして)いては逆効果です。滑らかに動かすには、アニメーションさせるプロパティ選びが決定的に重要です。結論はtransformとopacityだけでアニメーションさせること。理由を順を追って説明します。
ブラウザは画面を描くとき、大きく3つの工程を通ります。レイアウト(配置計算)→ ペイント(色塗り)→ コンポジット(合成)です。widthやleft、topを変えると、毎フレーム「配置計算」からやり直しになり、負荷が高くなります。
一方、transform(translate・scale・rotate)とopacityは、多くのブラウザで最後のコンポジット工程だけで処理でき、GPU(画像処理が得意な部品)に任せられるため、非常に軽く滑らかに動きます。だから、移動はleftではなくtranslateX、拡大はwidthではなくscaleで行うのが鉄則です。
| やりたいこと | 重い書き方(避ける) | 軽い書き方(推奨) |
|---|---|---|
| 左右に動かす | left / margin-left | transform: translateX() |
| 上下に動かす | top / margin-top | transform: translateY() |
| 大きさを変える | width / height | transform: scale() |
| 回転させる | (rotateは元々軽い) | transform: rotate() |
| 薄くする・消す | visibility の切替 | opacity |
この記事のサンプルの多くがtransformとopacityを使っているのは、この理由からです。移動や拡大縮小は必ずtransformで行い、leftやwidthのアニメーションは避ける、と覚えておくと、実務でスムーズに動くローディングが作れます。
「これから動く要素だよ」とブラウザに前もって伝えるwill-changeというプロパティもあります。ただし多用するとかえってメモリを圧迫して遅くなるため、初心者のうちは無理に使わなくて大丈夫です。まずはtransformとopacityでアニメーションする、という原則を守るだけで、ほとんどのローディングは十分滑らかに動きます。
種類が多いと「結局どれを使えばいいの?」と迷います。編集部の経験も踏まえ、シーン別のおすすめを整理しました。基本は「待ち時間の長さ」と「置く場所の広さ」で選びます。
大切なのは「待ち時間の見積もり」で使い分けることです。1〜2秒で終わりそうなら小さなスピナーで十分。数秒以上かかるなら、進捗や骨格を見せて「ちゃんと進んでいる」と伝えると、離脱を防げます。逆に、一瞬で終わる処理にわざわざ全画面オーバーレイを出すと、チカチカして煩わしく感じられるので避けましょう。
実際にローディングを実装するとき、編集部が確認している項目をチェックリストにまとめました。コピペしたあと、このリストで最終確認すると失敗が減ります。
finallyで消しているか)を確認したかrole="status"やaria-labelで読み上げソフトに状態が伝わるようにしたかprefers-reduced-motionでモーション配慮を入れたか最後に、ローディングアニメーションについて初心者の方からよく寄せられる質問にお答えします。実装でつまずいたときの参考にしてください。
親要素にdisplay: flex; align-items: center; justify-content: center;を指定するのが最も簡単で確実です。これでスピナーが上下左右の中央に配置されます。画面全体の中央に置きたい場合は、サンプル20のようにposition: fixed; inset: 0;を付けた要素の中でflexの中央寄せをします。margin: 0 auto;だけでは横方向しか中央にならないので、上下も中央にしたいときはflexかgrid(place-items: center)を使うと覚えておきましょう。
まず、width・height・left・top・marginをアニメーションしていないか確認してください。これらは毎フレーム配置計算が走るため重くなります。移動はtransform: translate()、拡大縮小はtransform: scale()に置き換えると、GPUで処理されて滑らかになります。詳しくは本文「パフォーマンス」の章を参照してください。それでも重い場合は、同時に動かす要素の数を減らす、box-shadowやfilter: blur()のアニメーションを避ける、といった対策も効果的です。
ほとんどの場合、非表示にするJavaScriptが呼ばれていないことが原因です。特に通信が失敗したときに消す処理を書き忘れているケースが多いです。本文で解説したとおり、非表示処理はtry...catchのfinallyブロックに書き、成功でも失敗でも必ず消えるようにしてください。また、非表示クラス(is-hidden)のCSSがdisplay: none;になっているか、クラス名のスペルが合っているかも確認しましょう。
スマホはPCよりも処理能力が限られるため、負荷の高いアニメーションは重く感じられます。対策は3つあります。1つ目はtransformとopacityだけでアニメーションすること。2つ目はbox-shadowやfilterを毎フレーム変化させないこと(影のアニメーションは特に重いです)。3つ目は、同時に大量のスピナーを表示しないことです。スケルトンスクリーンでたくさんの箱を光らせる場合は、シマーのアニメーションを1つの親でまとめるなどの工夫も有効です。
進捗の「見た目」はCSSだけで作れますが、実際の%を反映するにはJavaScriptが必要です。CSSはバーのwidthや円形プログレスの--valueを「滑らかに変化させる」役割を担い、その値を進捗に応じて更新するのはJavaScriptの仕事です。本文のサンプル7(確定プログレスバー)とサンプル15(円形プログレス)に、CSSとJavaScriptの両方の実装例を載せていますので参考にしてください。
「何秒出す」ではなく処理が終わるまで出し、終わったら即消すのが基本です。ただし、あまりに一瞬(0.2秒など)で消えるとチカチカして目障りなので、実務では「表示するなら最低でも0.3〜0.5秒は出す」といった最小表示時間を設けることもあります。逆に、数秒以上かかる処理では、単なるスピナーより進捗バーやスケルトンを使い、「あとどれくらいか」「ちゃんと進んでいるか」を伝えると、ユーザーの不安が減ります。
速度はanimationのduration(例:0.9s)の数字を変えます。数字を小さくすると速く、大きくするとゆっくりになります。色はbackgroundやborder-top-colorなど、色を指定している箇所を自分のブランドカラーに書き換えるだけです。サイズはwidth・heightやborderの太さで調整します。まずはコピペして動かし、これらの数字を少しずつ変えて挙動を確かめると、仕組みの理解が一気に進みます。
CSSローディングアニメーションを20種類、実装サンプルとともに解説してきました。数は多いものの、動きの正体は@keyframesで台本を書き、animationで再生するというシンプルな仕組みでした。回転・点滅・移動・伸縮の4つの組み合わせを覚えれば、あとは応用でいくらでもバリエーションを作れます。
そして忘れてはいけないのが、見た目だけでなく消し忘れ対策(finally)・アクセシビリティ・モーション配慮・パフォーマンスです。ここまで気を配れると、単に「動くローディング」ではなく「誰にとっても使いやすく、軽くて滑らかなローディング」になります。コピペで終わりにせず、この記事の原理まで押さえておけば、実務で自信を持って実装できるはずです。
・仕組み:ローディングは「@keyframes(動きの台本)」と「animation(再生指定)」の2つでできている。infiniteで回し続けるのが基本。
・実装:回転・点滅(opacity)・移動(translate)・伸縮(scale)の組み合わせ。JSはclassListの付け外しで表示切り替え、消す処理はfinallyに書く。
・配慮:role=”status”やaria-labelで読み上げに、prefers-reduced-motionでモーションに配慮。transformとopacityでカクつきを防ぐ。
とはいえ、CSSやJavaScriptを独学で体系立てて身につけるのは、意外と遠回りになりがちです。「コピペは動くけれど、なぜ動くのか自分で説明できない」「実務で通用するレベルまで自信を持てない」と感じる方は、正しい順序で学べる環境を活用するのが近道です。

副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。
公式サイト より
今すぐ
無料カウンセリング
を予約!