GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

バッファ 表示 アニメーション 9種 — スピナーから画質バッジまで

このバッファ 表示 9種のうち、1つは再生位置とバッファ位置を二重リングで一緒に描き、1つは波形をバッファされた分だけ明るく塗りつぶし、1つはバッファ不足で画質が下がったバッジを表示します。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

以下には9種類のバッファリング表示UIパターンが並んでいます。どれもライブラリなし・ビルド手順なしの単純なHTML・CSS・JSファイル1枚なので、zip内の好きなフォルダだけを取り出してindex.htmlをブラウザで直接開けます。

デモ

上のカードをクリックして止めたり、シークバーをドラッグしたり、画質を強制的に切り替えたりして、それぞれがどう振る舞うか確かめてみてください。以下の説明はすべて実際のデモのソースコードを読んで書いたもので、記憶を頼りに書いた説明ではありません。

9つのパターンの解剖 — どこで計算の一層が回るか

表の数値は上のデモが実際に描画したキャプチャそのものから来ています — 各パターンにつき24枚のフレームを同じ方法でサンプリングした結果です。累積面積は動いたピクセルが各フレームで占めた割合の合計、平均強度はピクセル値が実際に動いたフレームでの変化の強さ、動いたフレーム数(23中)は23回のフレーム間比較のうち実際に差があった回数です。

# パターン 累積面積 平均強度 動いたフレーム(23中)
01 静止フレーム上のバッファリングオーバーレイ 4.02% 71.5 20
02 再生・バッファ二重リング 1.78% 140.2 23
03 信号強度バーのバッファ表示 2.56% 141.2 13
04 バッファ分だけ塗られる波形 4.61% 168.3 23
05 停止検知後に揺れる再試行アイコン 10.05% 47.2 17
06 残り容量・速度から再計算する予想時間 1.79% 106.0 23
07 シークバーのバッファゴーストプレビュー 3.03% 143.0 23
08 帯域幅判定で変わる画質バッジ 1.53% 67.9 16
09 動作の低減オン・オフの並列比較 0.96% 110.3 23

9つの中で累積面積が最も広いのはパターン05の10.05%で、揺れるアイコンが往復する軌道が広いためです。反対に最も狭いのはパターン09の0.96%で、小さな二つのリングが並んで回るだけでは画面全体への影響がほとんど出ないからです。平均強度が最も高いのはパターン04の168.3で、9つの中で唯一150を超えています — マスクが波形の上で剥がれる動きは、回転や色のブレンドよりも動いたフレームあたりのピクセル変化が大きいためです。逆に平均強度が最も低いのはパターン05の47.2で、サンプリングされたフレームの多くが揺れの合間の静止状態を捉えているためです。パターン02・04・06・07・09はいずれも23回のフレーム間比較すべてで変化があり、9つの中で最多タイです。一方パターン03は13回にとどまり最少で、内部の保持ロジックが信号の変化中でもバーの表示をしばらく固定し続けるためです。

クラスタA — 合成サイン波が信号を模し、二層目がいつ信じるかを決める(01・03・06・08)

パターン01・03・06・08はいずれも同じ出発点を持ちます — Math.sin()を2〜3個組み合わせた関数がネットワーク状態(停止の揺れ・接続速度・転送速度・帯域幅の読み取り)の代わりを務め、実際のネットワークには一切触れません。違うのはその上に重ねる二層目です。01はその値をそのままぼかしと不透明度に流し込み、03と08はどちらも一定回数のティックやミリ秒だけ新しい値が続かないと表示を更新せず、06は残り量をその模した速度で割って秒数を出します。

クラスタB — 経過時間や固定の割合だけで一つの式が回る、センサーは関係ない(02・04・05)

パターン02・04・05はライブ信号らしきものを一切読みません — 02のリング形状は二つの独立した割合を一つの変換関数に渡すだけ、04の棒ごとの盛り上がりは進捗の割合に固定のイージング曲線を適用するだけ、05のアイコンの揺れは停止が閾値をどれだけ超えたかを減衰するサイン波に通すだけです。割合や経過時間を差し替えれば見た目は変わりますが、外部の何かを聞き取っているわけではありません。

クラスタC — 本物を測る二つのパターン(07・09)

パターン07は9つの中で唯一、実際のDOM要素を測定した値を核にしています — getBoundingClientRect()がトラックの実際の幅を返し、ポインタの実際のX座標をその幅で割った値がゴーストマーカーの位置になります。パターン09は9つの中で唯一、最初の確認のあとも聞き続けます — matchMediaのchangeハンドラがOS設定が実際に切り替わった瞬間にステータス文言を更新し、他の8つのように.matchesを一度だけ読んで終わるのとは違います。

パターン別コード

以下は9つそれぞれの完全なソースファイルから、実際に何が動くかを決めている計算の一層だけを取り出した短い断片です。

01静止フレーム上のバッファリングオーバーレイ

ストリーミングが途切れると画面上に半透明のオーバーレイが敷かれ、その上でスピナーが回る — 再生中だったフレームはそのまま静止して見える。

静止フレーム上のバッファリングオーバーレイ
新しいタブで開く

01 · 静止フレーム上のバッファリングオーバーレイ — 計測 累積面積 4.024% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

backdrop-filter@keyframesaria-live
  function tick(elapsedMs) {
    var phase = elapsedMs % STALL_CYCLE_MS;
    var stallSpan = STALL_CYCLE_MS * STALL_PORTION;
    var stalling = phase < stallSpan;
    if (stalling) {
      var stallFrac = clamp(phase / stallSpan, 0, 1);
      var spinMs = SPIN_MS_STALL_START - (SPIN_MS_STALL_START - SPIN_MS_STALL_END) * stallFrac;
      // 合成波2つでネットワークの揺れを再現する — Math.randomはレンダーごとに値が変わり測定を乱してしまう
      var jitter = 0.5 + 0.28 * Math.sin(elapsedMs / 190) + 0.14 * Math.sin(elapsedMs / 61);
      jitter = clamp(jitter, 0, 1);
      ring.style.setProperty('--spin-duration', spinMs.toFixed(0) + 'ms');
      overlay.style.setProperty('--blur', (4 + jitter * 7).toFixed(2) + 'px');
      overlay.style.opacity = (0.5 + jitter * 0.28).toFixed(2);
      if (status.textContent !== 'バッファリング中') status.textContent = 'バッファリング中';
    } else {
      overlay.style.opacity = '0';
      if (status.textContent !== '再生再開') status.textContent = '再生再開';
    }
  }

02再生・バッファ二重リング

円形リングを二重に重ね、外側リングは今までに受け取ったバッファ量を、内側リングは実際の再生位置を一緒に描く(329の単一リング+数字、035の単色ゾーン切り替えとは異なる実演 — バッファが再生位置よりどれだけ先行しているかが二つのリングの間隔で見える)。

再生・バッファ二重リング
新しいタブで開く

02 · 再生・バッファ二重リング — 計測 累積面積 1.778% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

conic-gradientstroke-dashoffset--buffer-pct
  // バッファ%と再生%を角度(グラデーション停止点)と周長(stroke-dashoffset)に変える算術
  function apply(bufferFrac, playFrac) {
    var bp = Math.round(bufferFrac * 100);
    bufferRing.style.setProperty('--buffer-pct', bp + '%');
    var offset = CIRC * (1 - playFrac);
    playValue.style.strokeDashoffset = offset.toFixed(2);
    bufferPctEl.textContent = bp + '%';
    wrap.setAttribute('aria-valuenow', String(bp));
  }

03信号強度バーのバッファ表示

信号の強さを表す4〜5本のバーがバッファリング速度に合わせて順に明るくなったり暗くなったりを繰り返し、点灯している本数で今の受信速度が速いか遅いかを示す。こうしたプログレスバー cssの多くは最終的に0から5までの一つの数値に還元されますが、このバーはその数値を画面に届ける方法が他の8つと違うだけです。

信号強度バーのバッファ表示
新しいタブで開く

03 · 信号強度バーのバッファ表示 — 計測 累積面積 2.558% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

--bar-leveltransform: scaleYsetInterval
  function tick(ms) {
    if (pinned !== null) return;
    var raw = rawSpeed(ms) * BAR_COUNT;
    var targetLevel = clamp(Math.round(raw), 0, BAR_COUNT);
    if (targetLevel !== displayLevel && Math.abs(raw - displayLevel) > CHANGE_MARGIN) {
      if (targetLevel === pendingLevel) {
        pendingCount++;
      } else {
        pendingLevel = targetLevel;
        pendingCount = 1;
      }
      if (pendingCount >= HOLD_TICKS) {
        displayLevel = pendingLevel;
        pendingCount = 0;
      }
    } else {
      pendingCount = 0;
      pendingLevel = displayLevel;
    }
    paint(displayLevel);
  }

04バッファ分だけ塗られる波形

オーディオ波形バーの上に半透明の膜を重ね、バッファされた区間の分だけその膜を剥がして本来の色を見せる(291のリアルタイム解析ビジュアライザーとは異なる実演 — ここではマイクやファイル解析なしに、バッファ進捗値ひとつだけで膜が剥がれる幅を計算する)。

バッファ分だけ塗られる波形
新しいタブで開く

04 · バッファ分だけ塗られる波形 — 計測 累積面積 4.609% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

mask-image--buffer-pctlinear-gradient
  function easeFn() {
    if (WAVEFORM_FILL_EASE === 'linear') return function (x) { return x; };
    var nums = WAVEFORM_FILL_EASE.replace('cubic-bezier(', '').replace(')', '').split(',').map(Number);
    return bezier(nums[0], nums[1], nums[2], nums[3]);
  }
  var ease = easeFn();

  function paint(pct) {
    var pctStr = (pct * 100).toFixed(1) + '%';
    film.style.setProperty('--buffer-pct', pctStr);
    wrap.setAttribute('aria-valuenow', String(Math.round(pct * 100)));
    var raw = pct * BAR_COUNT;
    var idx = Math.min(BAR_COUNT - 1, Math.floor(raw));
    var frac = raw - idx;
    bars.forEach(function (b, i) {
      var bump = i === idx ? Math.sin(frac * Math.PI) * BUMP_PX : 0;
      b.style.setProperty('--bump', bump.toFixed(1) + 'px');
    });
  }

05停止検知後に揺れる再試行アイコン

停止が一定時間(300ms)を超えると状態文言が「停止中」から「接続が切れて再試行中」に変わり、リトライアイコンが減衰するサイン波で短く揺れて収まる。ボタンを押すと即座に再生再開に戻る。

停止検知後に揺れる再試行アイコン
新しいタブで開く

05 · 停止検知後に揺れる再試行アイコン — 計測 累積面積 10.052% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateXsetIntervalis-stalled
  function setStatus(text) {
    if (status.textContent !== text) status.textContent = text;
  }

  // 停止経過時間(ms)を減衰サイン波の位相に変えて揺れ幅を計算する — CSSのkeyframeではなく毎ティックの算術
  function shakeOffset(stallOverMs) {
    var amp = SHAKE_AMP_PX * Math.exp(-SHAKE_DECAY * stallOverMs);
    return amp * Math.sin(stallOverMs * SHAKE_FREQ);
  }

06残り容量・速度から再計算する予想時間

毎ティック、残り容量を再計算した瞬間速度で割り直してETA表示を更新し、一時停止すると文言が止まり、受信完了で「受信完了」に変わる(349のリトライ待ちカウントダウンとは異なる実演 — こちらは失敗後の待機ではなく、今埋まっている速度から毎瞬残り秒数を再計算する)。

残り容量・速度から再計算する予想時間
新しいタブで開く

06 · 残り容量・速度から再計算する予想時間 — 計測 累積面積 1.794% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

performance.now--fill-pctsetInterval
  // 決定論的な合成波で瞬間転送速度を再現する — 毎ティック速度が変わるためETAは毎回再計算される
  function instSpeed(ms) {
    var w = 1 + SPEED_VARIANCE * Math.sin(ms / 480) + 0.2 * Math.sin(ms / 133);
    return Math.max(0.15, BASE_SPEED_MBPS * w);
  }

  function paint(speed) {
    var pct = Math.min(100, (loadedMb / TOTAL_MB) * 100);
    fill.style.setProperty('--fill-pct', pct.toFixed(1) + '%');
    track.setAttribute('aria-valuenow', String(Math.round(pct)));
    detail.textContent = loadedMb.toFixed(1) + 'MB / ' + TOTAL_MB.toFixed(1) + 'MB · ' + speed.toFixed(1) + 'MB/s';
    if (paused) {
      etaEl.textContent = '一時停止中';
    } else if (loadedMb >= TOTAL_MB) {
      etaEl.textContent = '受信完了';
    } else {
      // 残り容量 ÷ 今の速度 = 予想時間 — 保存したカウントダウンではなく毎ティック新しく割った値
      var remaining = TOTAL_MB - loadedMb;
      var etaSec = remaining / speed;
      etaEl.textContent = '約' + Math.ceil(etaSec) + '秒';
    }
  }

07シークバーのバッファゴーストプレビュー

シークバーをドラッグすると、getBoundingClientRectで実測したトラック幅を基準にしたポインタ比率がゴーストマーカーの位置と時間ラベルを計算し、再生・バッファ時間を「再生 0:00 ・ バッファ 0:00」の形式で表示する。

シークバーのバッファゴーストプレビュー
新しいタブで開く

07 · シークバーのバッファゴーストプレビュー — 計測 累積面積 3.027% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pointermove--ghost-pctgetBoundingClientRect
  // 実際のポインタ座標をトラック内の比率に変える計算(getBoundingClientRectで実測)
  function fracFromPointer(clientX) {
    var rect = track.getBoundingClientRect();
    return clamp((clientX - rect.left) / rect.width, 0, 1);
  }

  var dragging = false;

  track.addEventListener('pointermove', function (ev) {
    human();
    var f = fracFromPointer(ev.clientX);
    setGhost(f);
    if (dragging) {
      manualPlayFrac = f;
      paint(f, f);
    }
  });

08帯域幅判定で変わる画質バッジ

合成波で模した帯域幅を毎ティック高・中・低に判定するが、新しい判定がQUALITY_BADGE_HOLD_MS(1200ms)続いたときだけバッジの文字を変えて瞬間的な揺れを防ぎ、バッジの背景色はその間もliveColor()で実時間の信号を混ぜ続ける。

帯域幅判定で変わる画質バッジ
新しいタブで開く

08 · 帯域幅判定で変わる画質バッジ — 計測 累積面積 1.535% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

data-levelbackgroundColorQUALITY_BADGE_HOLD_MS
  // 帯域幅が閾値を超えた瞬間には切り替えず、その判定がQUALITY_BADGE_HOLD_MSの間ずっと続いたときだけ
  // ラベルとアイコンを変える — 瞬間的な急上昇でバッジの文字がちらつくのを防ぐ判定計算。
  // (背景色は上のliveColor()で毎ティック混ぜ続けるので、ライブ信号そのものが隠れるわけではない。)
  function tick(ms) {
    var mbps = bandwidth(ms);
    if (forced !== null) { paint(forced, mbps); return; }
    var target = judge(mbps);
    if (target !== displayLevel) {
      if (target === pendingLevel) {
        if (ms - pendingSinceMs >= QUALITY_BADGE_HOLD_MS) {
          displayLevel = target;
        }
      } else {
        pendingLevel = target;
        pendingSinceMs = ms;
      }
    } else {
      pendingLevel = displayLevel;
    }
    paint(displayLevel, mbps);
  }

09動作の低減オン・オフの並列比較

左パネルはtickMotion()で角度を連続計算してリングを回し続け、右パネルは同じ経過時間をREDUCED_STEP_MS(900ms)単位で区切ったtickReduced()で段階的にしか変えない — 実際のOSの動作の低減設定の変化もmatchMediaでリアルタイムに反映し、二つのパネルを並べて比較する。

動作の低減オン・オフの並列比較
新しいタブで開く

09 · 動作の低減オン・オフの並列比較 — 計測 累積面積 0.961% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

prefers-reduced-motionaria-livetextContent
  // 実際に使われる経路 — OSの本当の設定が変わった瞬間、このリスナーがすぐ文言を書き換える(リロード不要)
  function updateSystemStatus() {
    sysStatus.textContent = 'システム設定: 動作の低減 ' + (mq.matches ? 'オン' : 'オフ');
  }
  mq.addEventListener('change', updateSystemStatus);
  updateSystemStatus();

  // 左パネル(動作維持)は角度を連続計算して回転させ続け、右パネル(動作低減)は同じ経過時間の入力を
  // REDUCED_STEP_MS単位に区切って段階的にしか変えない — 両方を同じ時計と比べる計算。
  function tickMotion(ms) {
    var deg = (ms % SPIN_MS) / SPIN_MS * 360;
    ringMotion.style.transform = 'rotate(' + deg.toFixed(1) + 'deg)';
  }

  function tickReduced(ms) {
    var step = Math.floor(ms / REDUCED_STEP_MS) % 2;
    ringReduced.style.setProperty('--dim', step === 0 ? '1' : '.45');
  }

値がUIを変える場所 — 速度・閾値・保持時間・順序

html css ローディング アニメーションを探すと似た見た目のものはいくつも見つかりますが、下の変数を一つ変えるだけでパターンの印象がまるごと変わることを実際に確認できます。

01 — 揺れの位相

オーバーレイのぼかしと不透明度はどちらもtick()内のjitterを読んでいます — 二つの合成サイン波を足してから0〜1にクランプした値で、ある瞬間のオーバーレイの見た目はCSSの@keyframesの一コマではなく、式を計算した結果そのものです。

02 — 二つの割合

apply()はbufferFracとplayFracという二つの数値だけを受け取り、--buffer-pctと円周のstroke-dashoffsetに変換します。0〜1の数値を二つ渡せば、それがこのデモのタイマーから来たものでも実際のプレーヤーのバッファ範囲から来たものでも、両方のリングは即座に描き直されます。

03 — 保持カウント

tick()はtargetLevelが同じ値のままHOLD_TICKS(3回)連続でティックし続けないとdisplayLevelを変えません。これが、合成信号が境界をふらつくたびにバーの本数がちらつくのを防いでいる仕組みです。

04 — 塗りつぶしのイージング

WAVEFORM_FILL_EASEの値によってeaseFn()が返す関数が変わります — 文字列'linear'そのままか、cubic-bezier()を4つの数値に分解したものかのどちらかで、その一つの関数がpaint()内の各バーの--bump値の形を決めています。

05 — 停止の閾値

停止がSTALL_THRESHOLD_MS(300ms)を超えるまでは何も揺れません。超えた瞬間からshakeOffset()が超過分の経過時間を、SHAKE_AMP_PXで振幅を決めた減衰サイン波に変換し、停止が長引くほど振れ幅はゼロに近づいていきます。

06 — 毎回計算し直す残り

paint()はTOTAL_MB - loadedMbを、その瞬間instSpeed()が返す値で割っています。カウントダウンの文言は保存された数値がひとりでに減っていくのではなく、毎ティックその場で割り算し直した結果です。

07 — 実測される割合

fracFromPointer()はpointermoveのたびにgetBoundingClientRect()でトラックの実際の幅を取得し、カーソルのオフセットをその幅で割ります。ゴーストマーカーの位置は実際の要素を測ったライブな値であり、他の8つが頼っているタイマー駆動の値とは性質が違います。

08 — 保持時間

tick()はその判定がQUALITY_BADGE_HOLD_MS(1200ms)ぴったり続くまでdisplayLevelを新しい判定に動かしません。その間もバッジの背景色は同じティックの中でliveColor()によって連続的に混ざり続けます — ラベルは待たされますが、色は待たされません。

09 — 一つの時計、二つの式

tickMotion()とtickReduced()はどちらも毎フレーム同じmsの値を受け取ります。一方はそれを連続した角度に変えてrotate()に渡し、もう一方はREDUCED_STEP_MS(900ms)で割った商をそのまま0か1の段階に変えます。二つのパネルの違いは、どちらの時計を読むかではなく、どちらの式でその時計を読むかだけです。

どこで壊れるか — アクセシビリティ・reduced-motion・低速回線

パターン07のトラックはrole="slider"とtabindex="0"を持っているので、キーボードユーザーはすでに矢印キーで再生位置を動かせます — paint()はkeydownでもpointermoveと全く同じように実行されます。ただしpaint()は常に、今確定したばかりのplayFracと同じ割合でsetGhost()を呼ぶため、キーボードユーザーが見るゴーストマーカーは、すでに移動し終えた位置とぴったり重なって表示されます。ゴーストプレビューの存在意義そのもの — 確定する前にどこに着地するか見せること — は、クリックせずにホバーできるポインタにしか成り立ちません。キー入力は確定とプレビューが同時に起きるため、マウスを使わない瞬間、このパターンの目玉機能には見せるものが何も残らないのです。

動作の低減が残すもの

どのパターンも手動操作(ボタン・ドラッグ・矢印キー)はreduced motionの下でも変わらず動きます。止まるのは、あなたが触れなくてもデモを動かし続ける自動タイマーだけです。

  • 01 オーバーレイ — 揺れとスピン速度の変化が止まり、オーバーレイは不透明度0.6・ぼかし6pxに固定されて「バッファリング中」の文言が出続けます。
  • 02 二重リング — バッファ・再生のトゥイーンが止まり、apply()は62%/40%の固定値で一度だけ描かれます。
  • 03 信号バー — 合成信号の再サンプリングが止まり、レベル3(「信号良好」)に固定されます。
  • 04 波形の塗りつぶし — 塗りつぶしとバーごとの盛り上がりが止まり、paint(0.55)で55%バッファ済みの一場面が静止して描かれます。
  • 05 停止時の揺れ — アイコンの揺れだけが止まり、カードは停止状態と「停止中」の文言をそのまま表示し続けます。
  • 06 カウントダウンETA — 塗りバーのアニメーションが止まり、カウントダウンの文言は40%受信時点で計算した一つの値に固定されます。
  • 07 シークのゴースト — デモ自身の自動再生が止まり、両方のマーカーが一組の固定位置で静止します(ドラッグと矢印キーはタイマーに依存していないため、そのまま動き続けます)。
  • 08 画質バッジ — 判定ループが止まり、バッジは一つの帯域幅の読み取り値で「中画質」に固定されます。
  • 09 動作低減の比較 — このパターンだけは何も無効化されません。動作低減の状態そのものを見せることが目的だからです。updateSystemStatus()はどちらの場合もOSの実際の設定をそのままライブに反映し続けます。

これら9つのファイルはすべて同じ一つのパスワード、nj7m3t5r、を共有しています。一度zipを解凍すれば、上で見た9パターン分のHTML・CSS・JSがそのままディスク上に揃い、ブラウザで直接開いて確認できます。

3つのバリエーションとアクセシビリティ

バリエーション 値 感触
バッファリングの線幅を太くする 02 BUFFER_RING_STROKE_WIDTH 6px → 14px 二つのリングの間隔がずっと太い帯として見え、バッファが再生位置よりどれだけ先行しているかが数字なしでも一目でわかる — 細い時のわずかな差が、はっきりした余裕区間に変わる。
波形の塗りつぶしイージングを滑らかにする 04 WAVEFORM_FILL_EASE linear → cubic-bezier(.22,1,.36,1) 膜が剥がれる速度が終わりに向けて緩やかになり、バッファが追いついた瞬間が急に止まるのではなく柔らかく落ち着くように見える — 一定速度の時より到達が自然に感じられる。
画質低下バッジの保持時間を延ばす 08 QUALITY_BADGE_HOLD_MS 1200 → 3000 バッジが画面に留まる時間が二倍以上に伸び、バッファがすぐ回復しても画質が下がったことを読み取る余裕が生まれる — 短い時はちらっと消えるだけだった。

パターン01・03・06・08・09はそれぞれaria-pressedを持つ本物の<button type="button">なので、EnterキーとSpaceキーは追加のkeydownコードなしですでに切り替わります。パターン07のトラックだけはrole="slider"とtabindex="0"、そして専用の矢印キー処理を持っています — シークの位置は普通のボタンでは表現できない範囲の意味を必要とするからです。パターン02・04・06はどれもrole="progressbar"とライブに更新されるaria-valuenowで見た目を包み、パターン03のメーターはティックごとに更新されるラベル付きのrole="img"を持っています — この3つは読み取り専用の表示で、それ自体のキーボード操作は必要としません。

FAQ

これら9つのバッファリングアニメーションcssパターンは、実際のネットワーク接続や動画要素がないと動きませんか?

いいえ。9つとも完全にシミュレーションです — fetchを呼ばず、メディア要素のbufferedレンジも読まず、new Date()にも触れていません。上のバッファ・帯域幅・ネットワーク信号はすべてperformance.now()から計算した合成サイン波なので、完全にオフラインでも毎回同じ数値でテストできます。

prefers-reduced-motionがこのデザインで本当に尊重されているかはどう確認できますか?

OSのアクセシビリティ設定で「動作の低減」をオンにしてから上のデモを開き直してください。パターン01〜08は読み込み時に一度だけwindow.matchMedia('(prefers-reduced-motion: reduce)').matchesを確認して固定状態に切り替わりますが、パターン09だけはリロードなしに設定の変化を聞き続け、その仕組みはupdateSystemStatus()関数の中でそのまま確認できます。

この9種のプログレスバー デザイン cssはCodePenのようなものが必要ですか、それともダウンロードが必要ですか?

この9種のプログレスバー デザイン cssを見るのに外部の埋め込みは一切読み込まれません。zipをダウンロードすれば、9つのindex.htmlがそのまま解凍された状態で手元のディスクに残り、自由に編集できます。

出典: MDN — conic-gradient(), MDN — mask-image, MDN — Window.setInterval(), MDN — prefers-reduced-motion, MDN — aria-live。

関連記事: アップロード プログレスバー ローダー9種 — 計算するUI · ダウンロード 進捗バー 9選 — 値がパーセントで更新される9つの方式 · オーディオビジュアライザー 作り方 9選 — 棒からマイクまで

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。