円形パーセントローダー
conic-gradientとrequestAnimationFrameでなめらかにカウントアップする円形プログレス。イージングと完了時のグロー演出付き。初回ローディング画面に。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW:初回ローディング画面。円形パーセントが満ちると本編ヒーローへ -->
<div class="mb-stage" id="mbStage">
<!-- 本編(背面に常駐) -->
<main class="mb-hero">
<span class="mb-hero__tag">SPECIALTY COFFEE</span>
<h1 class="mb-hero__title">月夜に、<br>一杯の余白を。</h1>
<p class="mb-hero__lead">厳選した自家焙煎を、静かな夜にゆっくりと。</p>
<a class="mb-hero__btn" href="#">本日のおすすめ</a>
</main>
<!-- ローディングオーバーレイ -->
<div class="mb-load" id="mbLoad">
<div class="mb-ring" id="mbRing">
<div class="mb-ring__hole">
<span class="mb-ring__moon">☾</span>
<span class="mb-ring__pct" id="mbPct">0%</span>
</div>
</div>
<p class="mb-load__name">MOON BREW</p>
<p class="mb-load__sub">豆を挽いています…</p>
</div>
</div>
CSS
/* MOON BREW:円形パーセントローダー → 本編ヒーロー */
:root {
--cream: #f5ede1;
--brown: #2b1d12;
--amber: #c98a3b;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
overflow: hidden;
}
.mb-stage { position: relative; height: 400px; }
/* 本編ヒーロー */
.mb-hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 34px;
color: #fff;
font-family: "Hiragino Mincho ProN", serif;
background:
linear-gradient(180deg, rgba(43,29,18,0.5), rgba(43,29,18,0.78)),
url("https://picsum.photos/700/500?random=44") center/cover no-repeat;
}
.mb-hero__tag {
font-size: 10px;
letter-spacing: 0.3em;
color: var(--amber);
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
}
.mb-hero__title { margin: 12px 0; font-size: 32px; line-height: 1.4; font-weight: 700; }
.mb-hero__lead {
margin: 0 0 22px;
font-size: 13px;
line-height: 1.8;
color: rgba(255,255,255,0.9);
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
}
.mb-hero__btn {
align-self: flex-start;
padding: 11px 22px;
border-radius: 999px;
background: var(--amber);
color: #fff;
font-size: 13px;
font-weight: 700;
text-decoration: none;
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
box-shadow: 0 8px 20px rgba(201,138,59,0.4);
}
/* ローディングオーバーレイ */
.mb-load {
position: absolute;
inset: 0;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--cream);
transition: opacity 0.6s ease, visibility 0.6s ease;
}
.mb-load.is-done { opacity: 0; visibility: hidden; }
/* 円形プログレス:conic-gradient で塗り進む */
.mb-ring {
width: 124px;
height: 124px;
border-radius: 50%;
background:
conic-gradient(var(--amber) calc(var(--p, 0) * 1%), #e7dccb 0);
display: grid;
place-items: center;
margin-bottom: 16px;
}
.mb-ring.is-done { box-shadow: 0 0 26px rgba(201,138,59,0.55); }
.mb-ring__hole {
width: 98px;
height: 98px;
border-radius: 50%;
background: var(--cream);
display: grid;
place-items: center;
position: relative;
}
.mb-ring__moon {
position: absolute;
top: 20px;
font-size: 20px;
color: var(--amber);
}
.mb-ring__pct {
font-size: 26px;
font-weight: 700;
color: var(--brown);
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
margin-top: 12px;
}
.mb-load__name {
margin: 4px 0 0;
font-family: "Hiragino Mincho ProN", serif;
font-size: 17px;
font-weight: 700;
letter-spacing: 0.14em;
color: var(--brown);
}
.mb-load__sub { margin: 2px 0 0; font-size: 11px; color: #8a755e; letter-spacing: 0.08em; }
@media (prefers-reduced-motion: reduce) {
.mb-load { transition: none; }
}
JavaScript
// MOON BREW 初回ロード:conic-gradient を rAF でカウントアップ → 本編へ
const ring = document.getElementById('mbRing');
const pct = document.getElementById('mbPct');
const load = document.getElementById('mbLoad');
// イージング(終盤ゆっくり)
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
function run() {
if (!ring || !pct || !load) return;
load.classList.remove('is-done');
ring.classList.remove('is-done');
const dur = 2200;
const start = performance.now();
function tick(now) {
const t = Math.min((now - start) / dur, 1);
const v = Math.round(easeOut(t) * 100);
ring.style.setProperty('--p', v); // 塗り進み
pct.textContent = v + '%';
if (t < 1) {
requestAnimationFrame(tick);
} else {
// 完了:グロー → オーバーレイをフェードアウト
ring.classList.add('is-done');
setTimeout(() => load.classList.add('is-done'), 450);
// しばらくして再ループ
setTimeout(run, 4200);
}
}
requestAnimationFrame(tick);
}
// 初回起動
run();
コード
HTML
<!-- 円形プログレス: conic-gradient とカウントアップ数値で読み込み率を表現 -->
<div class="cp-stage">
<div class="cp-dial" id="cpDial" style="--p:0">
<div class="cp-inner">
<span class="cp-num" id="cpNum">0</span>
<span class="cp-pct">%</span>
</div>
</div>
<p class="cp-caption" id="cpCaption">アセットを読み込み中</p>
</div>
CSS
:root {
--bg: #0c1322;
--ring-bg: #1e2740;
--a1: #f9a8d4;
--a2: #c084fc;
--a3: #60a5fa;
--txt: #eef3ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background:
radial-gradient(700px 500px at 50% 120%, #1a1340 0%, transparent 60%),
var(--bg);
color: var(--txt);
}
.cp-stage {
display: grid;
justify-items: center;
gap: 20px;
padding: 24px;
}
/* conic-gradient で円弧を塗る。--p(0〜100) で角度が変わる */
.cp-dial {
width: 168px;
height: 168px;
border-radius: 50%;
background:
conic-gradient(
from -90deg,
var(--a1) 0%,
var(--a2) calc(var(--p) * .5%),
var(--a3) calc(var(--p) * 1%),
var(--ring-bg) calc(var(--p) * 1%)
);
display: grid;
place-items: center;
filter: drop-shadow(0 12px 30px rgba(96, 165, 250, .25));
}
.cp-inner {
width: 128px;
height: 128px;
border-radius: 50%;
background: var(--bg);
display: grid;
place-items: baseline center;
grid-auto-flow: column;
align-content: center;
justify-content: center;
gap: 2px;
box-shadow: inset 0 2px 10px rgba(0, 0, 0, .4);
}
.cp-num {
font-size: 46px;
font-weight: 700;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.cp-pct { font-size: 18px; color: rgba(238, 243, 255, .55); }
.cp-caption {
margin: 0;
font-size: 12px;
letter-spacing: .16em;
text-transform: uppercase;
color: rgba(238, 243, 255, .5);
}
.cp-dial.is-done { animation: cp-glow 1.4s ease-in-out infinite alternate; }
@keyframes cp-glow {
from { filter: drop-shadow(0 12px 30px rgba(96, 165, 250, .25)); }
to { filter: drop-shadow(0 12px 38px rgba(192, 132, 252, .55)); }
}
@media (prefers-reduced-motion: reduce) {
.cp-dial.is-done { animation: none; }
}
JavaScript
// requestAnimationFrame でなめらかにカウントアップする円形プログレス
const dial = document.getElementById('cpDial');
const num = document.getElementById('cpNum');
const caption = document.getElementById('cpCaption');
const DURATION = 2600; // ミリ秒
let startTime = null;
// イージング(終盤を緩める)
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
function frame(now) {
if (startTime === null) startTime = now;
const elapsed = now - startTime;
const t = Math.min(1, elapsed / DURATION);
const p = Math.round(easeOut(t) * 100);
if (dial) dial.style.setProperty('--p', String(p));
if (num) num.textContent = String(p);
if (t < 1) {
requestAnimationFrame(frame);
} else {
// 完了演出
dial?.classList.add('is-done');
if (caption) caption.textContent = '読み込み完了';
// 少し待ってからループ再生
setTimeout(() => {
dial?.classList.remove('is-done');
if (caption) caption.textContent = 'アセットを読み込み中';
startTime = null;
requestAnimationFrame(frame);
}, 1600);
}
}
// 初回起動
requestAnimationFrame(frame);
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「円形パーセントローダー」の効果を追加してください。
# 追加してほしい効果
円形パーセントローダー(ローダー & スケルトン)
conic-gradientとrequestAnimationFrameでなめらかにカウントアップする円形プログレス。イージングと完了時のグロー演出付き。初回ローディング画面に。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 円形プログレス: conic-gradient とカウントアップ数値で読み込み率を表現 -->
<div class="cp-stage">
<div class="cp-dial" id="cpDial" style="--p:0">
<div class="cp-inner">
<span class="cp-num" id="cpNum">0</span>
<span class="cp-pct">%</span>
</div>
</div>
<p class="cp-caption" id="cpCaption">アセットを読み込み中</p>
</div>
【CSS】
:root {
--bg: #0c1322;
--ring-bg: #1e2740;
--a1: #f9a8d4;
--a2: #c084fc;
--a3: #60a5fa;
--txt: #eef3ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background:
radial-gradient(700px 500px at 50% 120%, #1a1340 0%, transparent 60%),
var(--bg);
color: var(--txt);
}
.cp-stage {
display: grid;
justify-items: center;
gap: 20px;
padding: 24px;
}
/* conic-gradient で円弧を塗る。--p(0〜100) で角度が変わる */
.cp-dial {
width: 168px;
height: 168px;
border-radius: 50%;
background:
conic-gradient(
from -90deg,
var(--a1) 0%,
var(--a2) calc(var(--p) * .5%),
var(--a3) calc(var(--p) * 1%),
var(--ring-bg) calc(var(--p) * 1%)
);
display: grid;
place-items: center;
filter: drop-shadow(0 12px 30px rgba(96, 165, 250, .25));
}
.cp-inner {
width: 128px;
height: 128px;
border-radius: 50%;
background: var(--bg);
display: grid;
place-items: baseline center;
grid-auto-flow: column;
align-content: center;
justify-content: center;
gap: 2px;
box-shadow: inset 0 2px 10px rgba(0, 0, 0, .4);
}
.cp-num {
font-size: 46px;
font-weight: 700;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.cp-pct { font-size: 18px; color: rgba(238, 243, 255, .55); }
.cp-caption {
margin: 0;
font-size: 12px;
letter-spacing: .16em;
text-transform: uppercase;
color: rgba(238, 243, 255, .5);
}
.cp-dial.is-done { animation: cp-glow 1.4s ease-in-out infinite alternate; }
@keyframes cp-glow {
from { filter: drop-shadow(0 12px 30px rgba(96, 165, 250, .25)); }
to { filter: drop-shadow(0 12px 38px rgba(192, 132, 252, .55)); }
}
@media (prefers-reduced-motion: reduce) {
.cp-dial.is-done { animation: none; }
}
【JavaScript】
// requestAnimationFrame でなめらかにカウントアップする円形プログレス
const dial = document.getElementById('cpDial');
const num = document.getElementById('cpNum');
const caption = document.getElementById('cpCaption');
const DURATION = 2600; // ミリ秒
let startTime = null;
// イージング(終盤を緩める)
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
function frame(now) {
if (startTime === null) startTime = now;
const elapsed = now - startTime;
const t = Math.min(1, elapsed / DURATION);
const p = Math.round(easeOut(t) * 100);
if (dial) dial.style.setProperty('--p', String(p));
if (num) num.textContent = String(p);
if (t < 1) {
requestAnimationFrame(frame);
} else {
// 完了演出
dial?.classList.add('is-done');
if (caption) caption.textContent = '読み込み完了';
// 少し待ってからループ再生
setTimeout(() => {
dial?.classList.remove('is-done');
if (caption) caption.textContent = 'アセットを読み込み中';
startTime = null;
requestAnimationFrame(frame);
}, 1600);
}
}
// 初回起動
requestAnimationFrame(frame);
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。