CSS ローディング スピナー 9選 — コピペでOK
css ローディング スピナーは、読み込み中であることをひと目で伝える、CSSだけで動く小さな部品です。この記事では9種類を選び、疑似要素とtransform、そして@propertyという仕組みだけで実装しました。格子の9つはiframeの中で最初から回っていて、クリックでは止められません。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 基本リングスピナー
- 02 二重逆回転リング
- 03 3点バウンス
- 04 無限バー
- 05 conic-gradientスイープ
- 06 stroke-dashoffset円
- 07 @propertyで%リング
- 08 ボタン内ローディング状態
- 09 AIアプリ風パルスオーブ
並び順は効果の派手さではなく、回転を作る仕組みの難易度です。まずborder-top-colorだけで回す基本形から始め(01)、それを二重にして奥行きを足し(02)、形を点とバーに変え(03〜04)、conic-gradientとSVGのstroke-dashoffset、@propertyという発展的な書き方を重ね(05〜07)、最後にボタンの中とAIアプリ風の演出という実際の場面への転用を並べました(08〜09)。CSSだけでスピナーを完結させるため、08のJavaScriptはボタンの状態を切り替えるだけで、回転そのものの数値は一行も触っていません。
01基本リングスピナー
円形の枠線ひとつが上の一辺だけ色を変えて回り続け、読み込み中であることを知らせます。ページ全体の読み込みや、ボタンを押した直後の待機画面によく使われます。
.spinner {
width: clamp(48px, 14vw, 72px);
height: clamp(48px, 14vw, 72px);
border-radius: 50%;
border: 6px solid rgba($subject-cream, .22);
border-top-color: $color;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
02二重逆回転リング
内側と外側のリングが互いに逆方向へ回転し、平面のスピナーに立体的な奥行きを作ります。モーダルの読み込みや、データを取得中のダッシュボードカードで使うと効果的です。
.spinner__ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 5px solid transparent;
}
.spinner__ring--outer {
border-top-color: $color;
border-bottom-color: $color;
animation: spin 1.2s linear infinite;
}
.spinner__ring--inner {
inset: 13px;
border-left-color: $subject-blue;
border-right-color: $subject-blue;
animation: spin-rev .9s linear infinite;
}
@keyframes spin-rev {
to { transform: rotate(-360deg); }
}
033点バウンス
3つの点が少しずつ遅れて跳ね上がり、また沈むことで待機中であることを知らせます。チャットの入力待ちや、検索の自動補完を待つ場面に向いています。
.dots span {
width: clamp(12px, 4vw, 18px);
height: clamp(12px, 4vw, 18px);
border-radius: 50%;
background: $color;
animation: bounce .9s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: .16s; }
.dots span:nth-child(3) { animation-delay: .32s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); opacity: .5; }
40% { transform: translateY(-14px); opacity: 1; }
}
04無限バー
進捗の割合が分からないとき、短いバーが左右に伸び縮みしながら動き続け、進行中であることを示します。ファイルのアップロードが始まる前や、ページ上部の進捗表示に向いています。
.bar__fill {
position: absolute;
inset: 0;
border-radius: inherit;
background: $color;
transform-origin: left center;
animation: sweep 1.5s ease-in-out infinite;
}
@keyframes sweep {
0% { transform: translateX(-100%) scaleX(.3); }
50% { transform: translateX(20%) scaleX(.6); }
100% { transform: translateX(100%) scaleX(.3); }
}
05conic-gradientスイープ
conic-gradientひとつが背景画像を使わずに回転し、なめらかに薄れていくリングを描きます。ブランドカラーを強調したい読み込み画面や、決済を待つ画面に向いています。
.spinner {
width: clamp(56px, 16vw, 84px);
height: clamp(56px, 16vw, 84px);
border-radius: 50%;
background: conic-gradient($color 0deg 270deg, rgba($subject-cream, .18) 270deg 360deg);
mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
animation: spin 1.1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
06stroke-dashoffset円
SVGの円ひとつがstroke-dasharrayとstroke-dashoffsetの数値だけを変え、軌跡を描くように回ります。正確なパーセントを示さない待機アイコンや、アプリのスプラッシュ画面に向いています。
.spinner {
animation: spin 1.4s linear infinite;
transform-origin: center;
}
.spinner__arc {
stroke: $color;
stroke-linecap: round;
stroke-dasharray: 113;
stroke-dashoffset: 90;
animation: dash 1.4s ease-in-out infinite;
}
@keyframes dash {
0% { stroke-dashoffset: 100; }
50% { stroke-dashoffset: 20; }
100% { stroke-dashoffset: 100; }
}
07@propertyで%リング
@propertyで数値として登録したカスタムプロパティ--percentが0から100まで動き、conic-gradientの塗り分けを書き換えてリングを満たします。実際の進捗率がわかるアップロードや、オンボーディングの完了率の表示に向いています。MDNの@property対応表によると、@propertyはChrome 85以降・Safari 16.4以降・Firefox 128以降で使え、対応前のFirefoxでは--percentがアニメーションせず数字が固定されたまま表示されます。
@property --percent {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
.ring {
--percent: 0;
background: conic-gradient($color calc(var(--percent) * 1%), rgba($subject-ink, .18) 0);
animation: fill 1.8s ease-in-out infinite;
}
@keyframes fill {
0% { --percent: 0; }
50% { --percent: 100; }
100% { --percent: 0; }
}
08ボタン内ローディング状態
ボタンを押すとラベルの文字が消え、同じ場所に小さなスピナーが現れて処理中であることを示します。保存ボタンや決済ボタンに向いていて、幅をあらかじめ固定しておくことで文字が消えても横幅が縮みません。
var b = document.querySelector('.btn');
b.addEventListener('pointerdown', function () {
b.classList.remove('is-demo');
b.classList.add('is-loading');
b.disabled = true;
setTimeout(function () {
b.classList.remove('is-loading');
b.disabled = false;
}, 900);
});
09AIアプリ風パルスオーブ
にじんだグラデーションの球体が膨らんだり縮んだりを繰り返し、AIが応答を考え中であることを静かに伝えます。AIチャットボットの応答待ちや、音声アシスタントが聞き取っている最中の表示に向いています。
.orb::before {
position: absolute;
inset: -30%;
border-radius: 50%;
background: radial-gradient(circle, rgba($color, .55), transparent 70%);
filter: blur(6px);
animation: glow 2s ease-in-out infinite;
}
.orb::after {
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, $subject-cream, $color 75%);
animation: core 2s ease-in-out infinite;
}
@keyframes core {
0%, 100% { transform: scale(.88); opacity: .88; }
50% { transform: scale(1.08); opacity: 1; }
}
どこで崩れるか — 落とし穴
9つとも回転や拡大縮小をtransformだけで作っていて、位置やサイズの数値を直接書き換えるコードは使っていません。1つ目の落とし穴は07を組み立てているときに踏んだもので、@propertyに対応していないブラウザでは--percentのアニメーションだけが止まり、conic-gradientの塗り分けが0%のまま固定されて、リング全体が塗られない状態で止まって見えました。
2つ目は08のボタンを作ったときのことです。幅を固定していない状態で試すと、ラベルの文字が消えた瞬間にボタンの横幅がスピナーの直径まで縮み、周りのレイアウトが一瞬がたつきました。min-widthで幅を固定してから、この崩れは直りました。修正済みの9ファイルを開くには qda5pgmf という文字列が必要です。下のカードからzipを取ってください。
3つ目は06のSVGを組み立てたときのことです。stroke-dashoffsetだけをアニメーションさせて.spinnerの回転を外すと、円弧はその場で伸び縮みするだけで、リングの外周を回っているようには見えませんでした。.spinnerにも別のkeyframesで360度の回転を重ねたことで、軌跡を描きながら回る動きになりました。
アクセシビリティ
9つとも prefers-reduced-motion: reduce で回転や拡大縮小が止まりますが、要素そのものは消えません。基本リング(01)と二重リング(02)は静止した円のまま残り、07の@propertyリングは--percentを62に固定した部分的な塗りで止まり、09のオーブはぼやけた輪郭とopacity .75を保ったまま動きを止めます。
@media (prefers-reduced-motion: reduce) {
.spinner, .dots span, .bar__fill,
.spinner__arc, .ring, .orb::before,
.orb::after, .btn__spin { animation: none !important; }
.ring { --percent: 62; }
}
スピナー以外のCSSアニメーションはこちらのカテゴリー一覧に、このサイトの成り立ちは紹介ページにまとめています。
FAQ
9つを同じページに複数置いても動きは重なりませんか?
重なりません。それぞれのアニメーションは自分自身のクラス名にしか結び付いていないため、01のリングと09のオーブを隣に並べても、片方のanimationがもう片方の要素に影響することはありません。conic-gradientやmaskを使う05・07も、変数と背景がそれぞれの要素にスコープされているので同時設置に問題はありません。
Reactでは何を使って回転を表示しますか?
zipのreact/フォルダの各コンポーネントは、div要素かsvg要素にこれまでと同じクラス名を付けているだけで、回転の速さと曲線はCSSのanimationと@keyframesが担います。08だけはuseStateで処理中かどうかのbooleanを持ち、そのstateに応じてis-loadingに相当するクラス名を付け外しします。
スマホでも同じように回転しますか?
はい、01から06と09はiOSでも同じ速さで回転します。07の@propertyは前述のMDNの対応表にあるとおりSafari 16.4以降で動きますが、それより古いiOSでは--percentのアニメーションだけが止まり、リングは常に0%のまま表示されます。