DEMO
CODE
<div class="demo-stage dlb-stage">
<button type="button" class="dlb-btn" id="dlb-btn">
<span class="dlb-fill" id="dlb-fill" aria-hidden="true"></span>
<span class="dlb-label" id="dlb-label">ダウンロード</span>
</button>
<p class="dlb-status" id="dlb-status" role="status" aria-live="polite"></p>
</div>
.dlb-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.9rem; padding:2rem; background:#0B0B10; min-height:280px; }
.dlb-btn { position:relative; width:220px; height:52px; border:1px solid rgba(255,255,255,.18); border-radius:12px; overflow:hidden; cursor:pointer;
background:#17171C; color:#fff; font-family:'Inter',sans-serif; font-weight:600; font-size:.92rem; }
.dlb-btn:focus-visible { outline:2px solid #E91B89; outline-offset:3px; }
.dlb-btn[aria-busy="true"] { cursor:progress; }
.dlb-fill { position:absolute; inset:0; background:linear-gradient(90deg,#E91B89,#7B37A8); transform-origin:left center; transform:scaleX(0); }
.dlb-label { position:relative; font-variant-numeric:tabular-nums; }
.dlb-btn.is-done { border-color:rgba(52,211,153,.6); }
.dlb-btn.is-done .dlb-fill { background:#10B981; transition: background .3s ease; }
.dlb-status { margin:0; min-height:1.2em; color:#9A9AA6; font-size:.78rem; }
@media (prefers-reduced-motion: reduce) {
.dlb-btn.is-done .dlb-fill { transition-duration:.01ms; }
}
(function(){
var btn = document.getElementById('dlb-btn');
var fill = document.getElementById('dlb-fill');
var label = document.getElementById('dlb-label');
var status = document.getElementById('dlb-status');
if (!btn || !fill || !label) return;
var DURATION = 1800, busy = false;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
function finish(){
fill.style.transform = 'scaleX(1)';
btn.classList.add('is-done');
label.textContent = '✓ 完了';
status.textContent = 'ダウンロードが完了しました';
btn.removeAttribute('aria-busy');
setTimeout(function(){
btn.classList.remove('is-done'); fill.style.transform = 'scaleX(0)';
label.textContent = 'ダウンロード'; status.textContent = ''; busy = false;
}, 2200);
}
btn.addEventListener('click', function(){
if (busy) return;
busy = true;
btn.setAttribute('aria-busy', 'true');
status.textContent = 'ダウンロード中…';
if (reduce.matches) { finish(); return; }
var start = performance.now();
function tick(now){
var p = Math.min((now - start) / DURATION, 1);
var eased = 1 - Math.pow(1 - p, 3);
fill.style.transform = 'scaleX(' + eased + ')';
label.textContent = Math.round(eased * 100) + '%';
if (p < 1) requestAnimationFrame(tick); else finish();
}
requestAnimationFrame(tick);
});
})();
AI PROMPT
押すとボタンの中を進捗が満ちていき、終わるとチェックマーク付きの「完了」に変わるダウンロードボタンを作って。ボタンは幅と高さを固定し、overflow:hidden の中に絶対配置の塗り(グラデーション)を置いて transform: scaleX(0〜1)(transform-origin: left center)で伸ばす(width を動かすと周りのレイアウトが再計算されるので使わない)。進捗は requestAnimationFrame で経過時間から計算し、1 - (1 - p)^3 のイーズアウトで最後がゆっくりになるようにする。ラベルは「0%〜100%」を表示し、font-variant-numeric: tabular-nums で数字の幅をそろえて文字が揺れないようにする。完了したら塗りを緑に変え、ラベルを「✓ 完了」にし、2.2秒後に元へ戻す。処理中は aria-busy="true" を付けて二重クリックを無視し、状態は role="status" aria-live="polite" の段落で「ダウンロード中…」「ダウンロードが完了しました」と読み上げに伝える。prefers-reduced-motion: reduce の時は進捗を描かずにすぐ完了表示にする。実際のダウンロードでは fetch の ReadableStream で受信済みバイト数から p を計算する。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
押したボタン自身が進捗バーに変わり、完了すると結果を示すボタンです。ファイルのダウンロード・送信・書き出しのように数秒かかる処理で、「押せたのか」「今どこまで進んだのか」「終わったのか」をボタンの中だけで伝えられます。
当サイトの Progress Bar は独立した進捗バー、Copy Button は一瞬で終わる操作の完了表示、Bar Loader は進捗の分からない待ち時間の表示です。本ページは押した場所で進捗と完了までを見せる別テーマで、視線を動かさずに処理の状態を追えます。
要点は、幅を固定して transform で塗ること、処理中の二重クリックを防ぐこと、状態を読み上げにも伝えることの3点です。見た目の演出と実際の処理状態を必ず一致させ、進んでいないのに進捗が進むような表示はしないでください。
実際のダウンロードの進捗と連動させるには?
fetch で受け取った response.body(ReadableStream)を reader.read() で読み進め、受信したバイト数を Content-Length ヘッダーの値で割ると進捗 p が得られます。本デモの p を計算している部分をその値に置き換えるだけで、見た目の部分はそのまま使えます。Content-Length が無い応答では進捗が分からないため、不定の動き(ぐるぐる回る表示)に切り替えてください。
ボタンの幅を変える演出にしないのはなぜですか?
width を変えるとボタンの周りにある要素の位置も毎フレーム再計算され、動きがカクついたり、隣の文章がずれたりします。幅を固定して中の塗りを scaleX で伸ばせば、見た目は同じ進捗バーでもレイアウトは一切動きません。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。