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

css ゲージ 9選 — 計算で回る円形コントロール

9つの円形コントロールのうち、1つはドラッグした角度の値が慣性で減衰しつまみが徐々に止まり、1つは区間の値が角度に変換されてcss ゲージのバーが円を描きながら満ち、1つはスナップ判定の値によって最も近い区間へ弾けるように揃います。9つとも動かしているのは計算です。

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

下の各デモは本物の操作を待ってから初めて自動プレビューを止めます。pointerdown・クリック・矢印キーのいずれかがhuman()を通してisDemoをfalseに切り替えると、自動再生は二度と戻りません。9項目のうち02・04・05・08の4つはsetPointerCaptureを呼ぶため、ポインタがつまみの外に出てもドラッグの追跡が続き、09は矢印キー・Home・Endだけで操作でき、aria-valuenowが変更のたびに同期します。9項目ともMath.randomは一切使わず、ループの各ステップは経過時間か保存された角度に対する固定の式です。9項目すべてがprefers-reduced-motionを読み取りますが、実際の操作の後もその値を分岐に使い続けるのは04・05・06・07の4つだけで、残り5項目は自動プレビューの中でだけ固定の休止位置を取り、訪問者が実際に操作した後のreduced-motionはSCSS側のメディアクエリに任せます。

9つのパターンの解剖 — どの計算が円形コントロールを動かすか

表は項目ごとの測定値を3つ載せています。動くコントロールが累積でどれだけ画面を塗ったか、1ピクセルあたりの変化が平均でどれだけ強かったか、サンプリングした23フレームのうち実際にピクセルが動いたのは何フレームか、の3つです。

項目 累積面積 平均強度 動いたフレーム
01 11.25% 98.5 12/23
02 23.78% 120.2 23/23
03 11.20% 75.6 22/23
04 17.07% 55.4 17/23
05 1.66% 153.9 18/23
06 5.93% 181.2 23/23
07 18.51% 54.2 12/23
08 8.37% 88.8 23/23
09 2.96% 150.8 15/23

累積面積が最も広いのは項目02の23.78%だけで、円盤全体を回すと中央を通過するラベルの大きさと光り方がほぼ毎フレーム塗り直されるためです。最も狭いのは項目05の1.66%で、ドラッグの合間はつまみ本体と短い弧だけが動き、ダイヤル自体は静止したままだからです。平均強度で最も高いのは項目06の181.2で、conic-gradientのリングがGAUGE_STEP_DEGの倍数に丸まるたびに輪郭の立った段差で塗り直されるためで、なめらかなフェードとは違います。最も低いのは項目07の54.2で、スプリングが収束する間、つまみのスケールは1フレームごとにわずか数パーセントしか変わりません。動いたフレームが最も多いのは項目02・06・08の23/24で、最も少ないのは項目01・07の12/24です。この差は、各パターンが休止せず新しく描く何かを常に持っているかどうかをそのまま映しています。

値を動かす計算ごとに分けると、9項目は4つの系統に分かれます。

01 — ポインタを読まず一度だけ計算する固定レイアウト

項目01はポインタの位置を一度も読みません。各項目の位置は、インデックスをARC_SPAN_DEG内に割り振った角度からMath.cosとMath.sinをRADIUS倍する極座標→直交座標変換1回だけで決まり、メニューを組み立てるときに一度計算されたまま、開閉しても変わりません。

02・03・08 — ポインタから毎回読み取る角度

項目02はドラッグを回転角に変え、STEP_DEGの剰余で今どのスロットが中央にあるかを求めます。項目03はatan2でポインタ位置を方位角に変換し、N個の区間のどれに落ちるかを切り下げで判定します。項目08も同じくatan2を使いますが、結果を半円に丸め、その0〜1の位置を座標と色相の両方に使います。

04・05・07 — 手を離した後も動き続ける物理

項目04は、離した瞬間の速度が毎フレームDRAG_DECAY倍に縮んでMIN_VELOCITYを下回るまで回転し続けます。項目05は、離した角度と最も近い目盛りとの距離を比べ、SNAP_THRESHOLD_DEG以内なら目標をわずかに超えてから戻る曲線でそこへ寄せます。項目07はスケール値をスプリングで動かし、目標までの距離にSPRING_Kを掛けた力とSPRING_DAMPによる減衰で、つまみが押し込まれてから跳ね返るようにします。

06・09 — 値をそのまま角度に量子化する

項目06はパーセントを角度に変換し、最も近いGAUGE_STEP_DEGの倍数まで切り下げて、なめらかな掃引ではなく目に見える段差でリングを満たします。項目09はパーセントをRANGE_DEG内の角度に変換し、矢印キーが押されるたびに固定のSTEP_DEG分だけ動かし、aria-valuenowがそのたびに正確な数値を知らせます。

パターン別コード

01放射状メニュー — 中心から扇形に広がる円形メニュー

各項目の位置は組み立て時の1回の計算で固定されます。角度はARC_SPAN_DEG(200)の範囲内でインデックスiを均等に割り振り、fabの上に中心を置いた角度でMath.cosとMath.sinをRADIUS(96)倍して--xと--yのカスタムプロパティにピクセル単位で渡します。fabのクリックはrm--openクラスと各項目のtabIndexを0と-1の間で切り替え、Escapeキーは2回目のクリックと同じようにメニューを閉じます。

数値が語ること

項目01は動いたフレームが12/24と9項目の中で少ない側にあり、累積面積は11.25%、平均強度は98.5です。レイアウトの計算は1回で済むため、実際に塗り直されるのは開閉時の段差付き表示だけで、フレームごとの再計算ではありません。

放射状メニュー — 中心から扇形に広がる円形メニュー
新しいタブで開く

01 · 放射状メニュー — 中心から扇形に広がる円形メニュー — 計測 累積面積 11.25% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ARC_SPAN_DEGRADIUStranslate
  // 計算層: 極座標→直交座標変換。項目iは上部(-90度)を中心にARC_SPAN_DEGを均等分割した
  // 角度の位置からRADIUS分離れた場所を占める。この座標は開閉状態に関係なく一度だけ計算する。
  var items = ACTIONS.map(function (label, i) {
    var li = document.createElement('li');
    li.className = 'rm__item';
    li.setAttribute('role', 'none');
    var btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'rm__itemBtn';
    btn.setAttribute('role', 'menuitem');
    btn.tabIndex = -1;
    btn.textContent = label;
    var angleDeg = -90 - ARC_SPAN_DEG / 2 + i * (ARC_SPAN_DEG / (ACTIONS.length - 1));
    var rad = angleDeg * Math.PI / 180;
    var x = Math.round(Math.cos(rad) * RADIUS);
    var y = Math.round(Math.sin(rad) * RADIUS);
    li.style.setProperty('--x', x);
    li.style.setProperty('--y', y);
    li.style.setProperty('--stagger', i);

02ジャンル選択ホイール — カテゴリを円盤のように回して選ぶ方式

ドラッグは円盤全体をポインタの角度差分だけ回転させ、activeIndex()は回転角を反転してSTEP_DEGで割った剰余から、直近に中央を通過したスロットを求めます。apply()が呼ばれるたびに、ドラッグでも自動ループでも、選択中のジャンル名がラベルに反映されます。

数値が語ること

項目02は累積面積23.78%で9項目中最も広く、動いたフレームも23/24で最多タイに並びます。円盤を回し続けるとどのスロットが拡大されるかが常に変わり、回転そのものも毎フレーム塗り直されるためです。

ジャンル選択ホイール — カテゴリを円盤のように回して選ぶ方式
新しいタブで開く

02 · ジャンル選択ホイール — カテゴリを円盤のように回して選ぶ方式 — 計測 累積面積 23.781% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

activeIndexSTEP_DEGhsl(
  // 計算層: 回転角をSTEP_DEGで割った余りから「現在中央を通過しているインデックス」を求める。
  function activeIndex() {
    var norm = ((-rotation % 360) + 360) % 360;
    return Math.round(norm / STEP_DEG) % N;
  }

  function apply() {
    ring.style.setProperty('--rotation', rotation);
    var idx = activeIndex();
    lis.forEach(function (li, i) { li.classList.toggle('gw__slot--active', i === idx); });
    picked.textContent = '選択: ' + GENRES[idx];
  }

03セグメントホバーリング — ポインタが通る区間だけ際立つ円形の帯

angleAt()はatan2でポインタ位置を方位角に変換し、12時方向を0度とするようにずらします。segmentIndexAt()はその角度を360/Nで切り下げて、ポインタがN個のうちどの区間にいるかを求めます。変化するのはその1区間の太さと明るさだけで、単一のactiveクラスで切り替わり、ポインタが離れると解除されます。

数値が語ること

項目03は動いたフレーム22/24で上位に並び、平均強度75.6・累積面積11.20%は、ポインタを常時追いかけているにもかかわらず一度だけ計算する項目01とほぼ同じ広さにとどまります。

セグメントホバーリング — ポインタが通る区間だけ際立つ円形の帯
新しいタブで開く

03 · セグメントホバーリング — ポインタが通る区間だけ際立つ円形の帯 — 計測 累積面積 11.2% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

segmentIndexAtMath.atan2conic-gradient(
  // 計算層: ポインター座標をリング中心基準の角度に変換した後(atan2)、どの区間(360/N幅)に属するかを判定する。
  function angleAt(clientX, clientY) {
    var r = ring.getBoundingClientRect();
    var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
    var deg = Math.atan2(clientY - cy, clientX - cx) * 180 / Math.PI + 90; // 12時方向を0度として
    return ((deg % 360) + 360) % 360;
  }
  function segmentIndexAt(deg) {
    return Math.floor(deg / (360 / N)) % N;
  }

04ドラッグ慣性つまみ — 手を離しても徐々に止まる回転式つまみ

ノブを離してもそこで止まりません。decayStep()は直前の速度を毎フレームDRAG_DECAY倍しながら角度に足し続け、速度がMIN_VELOCITYを下回るまでつまみは自らの慣性で回り続けます。reduced motionでは同じ関数が速度を即座にゼロにし、訪問者が実際に操作した後でもこの分岐が適用されて減衰区間そのものを飛ばします。

数値が語ること

項目04は累積面積17.07%で9項目中2番目に広く、平均強度は55.4と中程度、動いたフレームは17/24です。減衰カーブは離した後も数フレーム塗り直しを続けるため、すぐには静止しません。

ドラッグ慣性つまみ — 手を離しても徐々に止まる回転式つまみ
新しいタブで開く

04 · ドラッグ慣性つまみ — 手を離しても徐々に止まる回転式つまみ — 計測 累積面積 17.067% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

decayStepDRAG_DECAYMath.atan2
  // 計算層: 慣性減衰。離した瞬間の速度が毎フレームDRAG_DECAY倍に縮み、角度に足され続ける。
  // reduce時は(実操作後も含めて)減衰区間を飛ばして即座に停止する — human()の後もこの分岐はそのまま適用される。
  function decayStep() {
    if (dragging) return;
    if (reduce) { velocity = 0; return; }
    if (Math.abs(velocity) < MIN_VELOCITY) { velocity = 0; return; }
    velocity *= DRAG_DECAY;
    angle += velocity;
  }

05スナップ選択ダイヤル — 近い目盛りへ弾けるように揃う方式

離した瞬間、nearestNotch()は落とした角度とN個の目盛りそれぞれとの最短角距離を測り、その距離がSNAP_THRESHOLD_DEG以内ならわずかに目標を超えてから戻る曲線でその目盛りへ寄せます。同じeaseOvershoot()の曲線は自動デモループのドラッグ→整列サイクルも動かしています。

数値が語ること

項目05は平均強度153.9で9項目中2番目に高い一方、累積面積は1.66%と9項目中最も狭く、動いたフレームは18/24です。つまみそのものはコントラストの強い動きをしますが、ダイヤルのごく小さな範囲だけで完結します。

スナップ選択ダイヤル — 近い目盛りへ弾けるように揃う方式
新しいタブで開く

05 · スナップ選択ダイヤル — 近い目盛りへ弾けるように揃う方式 — 計測 累積面積 1.661% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

nearestNotchSNAP_THRESHOLD_DEGNOTCH_DEG
  // 計算層: 離した角度と最も近い目盛り(N等分)との最短角距離を求め、しきい値内ならその目盛りにスナップする。
  function nearestNotch(deg) {
    var idx = Math.round(deg / NOTCH_DEG) % N;
    var notchDeg = idx * NOTCH_DEG;
    var dist = Math.abs(((deg - notchDeg + 540) % 360) - 180);
    return { idx: idx, notchDeg: notchDeg, dist: dist };
  }

  function apply(a) {
    ring.style.setProperty('--angle', a);
  }

  function startSettle(target) {
    settling = true;
    settleFrom = angle;
    settleTo = target;
    settleStart = performance.now();
  }

  function easeOvershoot(t) {
    // 目標をわずかに超えてから戻る弾み(easeOutBack) — cubic-bezierでは作れない符号反転カーブ
    var c1 = 1.70158, c3 = c1 + 1;
    var t1 = t - 1;
    return 1 + c3 * t1 * t1 * t1 + c1 * t1 * t1;
  }

06円形ゲージバー — 値に応じて角度が満ちる円形の進捗表示

quantizedDeg()は値を角度に変換し、最も近いGAUGE_STEP_DEGの倍数まで切り下げるため、conic-gradientのリングはなめらかな掃引ではなく目に見える段差で満ちます。表示値が目標へ近づく速さは別の0.16の補間係数が担っており、この段差の単位とは完全に独立しています。2つのボタンが目標値をSTEP_PCT(10)ずつ動かし、自動ループは0から100までリングを満たしてから最初に戻ります。

数値が語ること

項目06は平均強度181.2で9項目中最も高く、動いたフレームも23/24で最多タイです。満ちる段差が輪郭の立ったまま切り替わるたびに、前の段と混ざらない新しいくさび形を塗り直すためです。

円形ゲージバー — 値に応じて角度が満ちる円形の進捗表示
新しいタブで開く

06 · 円形ゲージバー — 値に応じて角度が満ちる円形の進捗表示 — 計測 累積面積 5.931% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

quantizedDegGAUGE_STEP_DEGconic-gradient(
  // 計算層: パーセントを角度に変換した後、GAUGE_STEP_DEGの倍数に切り下げる。
  // なめらかな360*value/100の代わりに階段状に見せ、「ステップ型」ゲージになる。
  function quantizedDeg(v) {
    var raw = Math.min(100, Math.max(0, v)) * 3.6;
    return Math.floor(raw / GAUGE_STEP_DEG) * GAUGE_STEP_DEG;
  }

  function apply() {
    ring.style.setProperty('--deg', quantizedDeg(shown));
    pctEl.textContent = Math.round(shown) + '%';
  }

  function setValue(v) { value = Math.min(100, Math.max(0, v)); }

07物理感ノブダイヤル — つまみ自体が押し込まれ跳ね返る質感のダイヤル

springStep()は距離にSPRING_Kを掛けた力でスケール値を目標へ押し出し、積算した速度に毎フレームSPRING_DAMPを掛けて収束させるため、つまみは静止サイズを一度超えてから跳ね返って戻ります。押している間は目標を1未満に、離すと1より大きく設定し、スプリングが最終的にスケール1へ落ち着きます。

数値が語ること

項目07は累積面積18.51%と02・04に次ぐ広さながら、平均強度は54.2と9項目中最も低くなっています。動いたフレームは12/24で計算自体は続いていますが、スプリングがつまみのスケールを1フレームあたりわずか数パーセントしか変えないためです。

物理感ノブダイヤル — つまみ自体が押し込まれ跳ね返る質感のダイヤル
新しいタブで開く

07 · 物理感ノブダイヤル — つまみ自体が押し込まれ跳ね返る質感のダイヤル — 計測 累積面積 18.514% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

springStepSPRING_KSPRING_DAMP
  // 計算層: 減衰スプリング。毎フレーム目標までの距離にSPRING_Kを掛けて加速し、
  // 積算速度にSPRING_DAMPを掛けて徐々に収束させる(単純なlerpと違い、目標を超えて跳ね返る)。
  function springStep() {
    if (reduce) { scaleCur = scaleTarget; scaleVel = 0; return; }
    var force = (scaleTarget - scaleCur) * SPRING_K;
    scaleVel = (scaleVel + force) * SPRING_DAMP;
    scaleCur += scaleVel;
  }

  function apply() {
    knob.style.setProperty('--scale', scaleCur.toFixed(4));
    valueEl.textContent = '明るさ' + LEVELS[levelIdx] + '%';
  }

08円弧ムードスライダー — 半円に沿って強さを選ぶつまみ

angleToT()はatan2でポインタ位置を方位角に変換し、上側の半円に丸めてから0〜1の位置に正規化します。apply()はその同じ位置を、つまみの座標・色相・ムードラベルのインデックスへ同時に読み替えます。矢印キーは同じ経路を1/(N-1)刻みで動かし、ドラッグとキー操作の結果を一致させます。

数値が語ること

項目08は動いたフレーム23/24で最多タイ、累積面積8.37%・平均強度88.8です。つまみの位置・色・ラベルがポインタやキー操作のたびにそろって更新されます。

円弧ムードスライダー — 半円に沿って強さを選ぶつまみ
新しいタブで開く

08 · 円弧ムードスライダー — 半円に沿って強さを選ぶつまみ — 計測 累積面積 8.369% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

angleToTMath.atan2hsl(
  // 計算層: ポインター座標をatan2で半円(180~360deg、左端~右端)の角度に変換し、0~1に正規化する。
  function angleToT(clientX, clientY) {
    var r = arc.getBoundingClientRect();
    var cx = r.left + r.width / 2, cy = r.bottom;
    var deg = Math.atan2(clientY - cy, clientX - cx) * 180 / Math.PI; // -180~0(上側の半円)
    var clamped = Math.min(0, Math.max(-180, deg));
    return 1 - (clamped + 180) / 180;
  }

  function apply() {
    var deg = -180 + t * 180;
    var x = Math.cos(deg * Math.PI / 180);
    var y = Math.sin(deg * Math.PI / 180);
    handle.style.setProperty('--x', x.toFixed(4));
    handle.style.setProperty('--y', y.toFixed(4));
    handle.style.setProperty('--hue', Math.round(t * 130));
    var idx = Math.min(N - 1, Math.floor(t * N));
    labelEl.textContent = MOODS[idx];
  }

09キーボード操作円形コントロール — 矢印キーで目盛りを動かすアクセシブルダイヤル

valueToDeg()は0〜100の値をRANGE_DEG(300)度のリングへ写し、矢印キーを1回押すたびに、STEP_DEG(12)をRANGE_DEGで割った一定の比率分だけ値が動きます。aria-valuenowとaria-valuetextは変化のたびに正確なパーセントへ同期します。自動ループもキー入力と同じ刻み幅で値を動かし、違いはkeydownイベントの代わりにタイマーで進む点だけです。

数値が語ること

項目09は平均強度150.8で9項目中3番目に高く、累積面積は2.96%と狭め、動いたフレームは15/24です。1回の操作で塗り直されるのは新しい角度のつまみだけで、リング全体ではありません。

キーボード操作円形コントロール — 矢印キーで目盛りを動かすアクセシブルダイヤル
新しいタブで開く

09 · キーボード操作円形コントロール — 矢印キーで目盛りを動かすアクセシブルダイヤル — 計測 累積面積 2.96% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

valueToDegSTEP_DEGaria-valuenow
  // 計算層: 値(%)を角度に変換する。RANGE_DEG範囲内で0%→開始角、100%→終了角。
  function valueToDeg(v) {
    return -RANGE_DEG / 2 + (Math.min(100, Math.max(0, v)) / 100) * RANGE_DEG;
  }

  function apply() {
    handle.style.setProperty('--deg', valueToDeg(value));
    handle.setAttribute('aria-valuenow', String(Math.round(value)));
    handle.setAttribute('aria-valuetext', Math.round(value) + '%');
  }

  function setValue(v) { value = Math.min(100, Math.max(0, v)); apply(); }

  // 計算層: キー入力1回 = STEP_DEG分の角度 = RANGE_DEGに対するその比率分のパーセント。
  var stepPct = (STEP_DEG / RANGE_DEG) * 100;

値がつまみを変える基準 — 角度・慣性・スナップ・フォーカス

一段を決める定数

各項目の反応は名前付きの定数で決まります。01のARC_SPAN_DEG(200)とRADIUS(96)、02のSTEP_DEG、03のN個の区間、04のDRAG_DECAY(0.92)とMIN_VELOCITY、05のNOTCH_DEGとSNAP_THRESHOLD_DEG(20)、06のGAUGE_STEP_DEG(6)とSTEP_PCT(10)、07のSPRING_K(0.18)とSPRING_DAMP(0.72)、08のN個のムード、09のSTEP_DEG(12)とRANGE_DEG(300)です。css ゲージ デザインを探すと06のようなリング状の進捗表示だけを思い浮かべがちですが、この9項目で実際に調整する価値があるのはリングの塗り方だけでなく、これらのパターンごとの定数そのものです。参考までに、06の満ちる速さを決める補間係数(0.16)とLOOP_MSはGAUGE_STEP_DEGとは完全に別物です — 段差を小さくすると同じ速さのまま階段状の満ち方がより滑らかに見えるだけで、満ちる速度自体は変わりません。

近い記事 — ゲージやスライダーと円形コントロールの境界

扇形の展開レイアウトを持たない単純なパーセント表示はCSS プログレスバー 円形ゲージ9選 — 数値を変えるだけが扱っており、ここの06はその系統に近いものの、他の8項目はポインタやキーで動かせるつまみを持つ点で異なります。canvasで回るプログレスバー css アニメーションの例を探して来た読者には、代わりにweb ルーレット — 弾いて回し摩擦で止まる回転盤キャンバス9種が合います — ここの02は固定軸上でDOMの円盤を角度計算だけで回しますが、332のルーレットは摩擦と発射速度を持つcanvasの物理シミュレーションで、描画の層そのものが違います。直線のドラッグとスナップはcss レンジスライダー — 慣性・スナップ計算を重ねたカスタム9種が扱っており、ここの04・05の慣性と目盛りの計算を、直線のトラックの代わりに円へ曲げたものと言えます。

どこで壊れるか — タッチ・キーボード対応、フォールバック、reduced-motion

Pointer Eventsはマウス・タッチ・ペンを1つのリスナー集合でまとめて扱いますが、setPointerCaptureを呼ぶのは02・04・05・08の4項目だけで、つまみ自身の外までドラッグの追跡が続きます。残りは01・06のように1回のクリックだけを読むか、09のようにフォーカスと矢印キーにしか反応しません。キーボード対応も一様ではなく、09は矢印キーだけで完全に操作でき、HomeとEndで両端へ飛べます。01はEscapeで閉じ、02と08はドラッグの代わりに矢印キーを受け付け、07はEnterまたはSpaceで押し込み→離すの動作を起こします。一方03・04・05は現時点でフォーカスを受け取るだけで専用のキーハンドラを持ちません。プログレスバー css アニメーションのコードだけでなくビルド不要でそのまま貼れるコードを探している人にとって、上のコードフェンスと下のzipが両方とも切り詰めのない完全なソースを持つ理由はここにあります。

9項目すべてのマークアップ・スタイル・スクリプトが一式そろって、下のzip内でパスワードydmj5nhaの奥に置かれています。開けば定数ごとにそのまま調整できます。

reduced motionが変えないもの・変えるもの

実際に訪問者が操作した後もreduceフラグを読み続けるのは4項目だけです。04はドラッグが終わった瞬間に速度をゼロにして減衰区間を丸ごと飛ばし、05は整列にかける時間をゼロにして目盛りへ直接スナップし、06は補間係数を1に固定して表示値が目標へ一気に飛ぶようにし、07はスケールを即座に確定させて押し込み→離すのタイムアウトもゼロにするため、スプリングのサイクルが一切再生されません。残る5項目 — 01・02・03・08・09 — は自動プレビューのループの中でだけreduceを分岐に使います。reduceが有効で操作前の状態では、01は0.12〜0.62の開閉フェーズを回さず開いたままで、02は回転角ゼロで静止し、03は最初の区間を選んだままで、08はつまみを弧の中間に置き、09は値を40%で保持します。しかし一度この5項目のどれかに訪問者がドラッグ・クリック・キー操作を行うと、そのJavaScriptはもうreduceを一切参照せず、実際の操作が起こすはずだったトランジションを取り除くのは各項目のSCSS側にあるreduced-motionのメディアクエリの役目になります。

9項目すべてを1ページに並べると、requestAnimationFrameのループが同時に9本走ることになり、非力な端末ではフレームが落ちることがあります。実際に組み込むときは、必要な項目1つだけを残すのが安全な選び方です。

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

バリエーション 変わるもの 変わらないもの
ドラッグ慣性を長く残す 04 DRAG_DECAY 0.92 → 0.98 MIN_VELOCITY、惰性で止まる挙動そのもの、reduced motionでの即停止
ゲージの満ちる表現をより滑らかに 06 GAUGE_STEP_DEG 6 → 3 0.16の補間係数、LOOP_MS、STEP_PCT
スナップ判定の範囲を狭める 05 SNAP_THRESHOLD_DEG 20 → 10 NOTCH_DEG、目標を超えてから戻る曲線、SETTLE_MS

04のDRAG_DECAYを0.92から1に近づけると、decayStep()で速度に毎フレーム掛かる減衰率が大きくなり、離した後の速度低下が緩やかになって慣性がより長く残ります。方向とMIN_VELOCITYによる打ち切りはそのままです。06のGAUGE_STEP_DEGを6から3に下げても、quantizedDeg()が表示角度を丸める単位が変わるだけで、満ちる速さそのものには関係しません。速さは0.16の補間係数とLOOP_MSという別の値が決めており、この定数には左右されません— 段差が細かくなることで、同じ階段状の満ち方がより滑らかに見えるだけです。05のSNAP_THRESHOLD_DEGを20から10に狭めると、nearestNotch()が測る距離(n.dist)がこの値以下のときだけスナップが働くようになり、目盛りへより近づいてからでないとスナップがかからなくなって、離す位置に対して厳しくなります。

キーボードだけで完全に操作できるのは02・06・07・08・09です。02は矢印キーがSTEP_DEG分ずつホイールを回し、06の2つのボタンはネイティブのbutton要素で追加のスクリプトを必要とせず、07はEnterとSpaceがポインタと同じ押し込み→離すの動作を起こし、08は矢印キーが弧上の位置を一定の刻みで動かし、09はrole="slider"を持つ完全なコントロールで矢印キー・Home・Endがすべて setValue() に配線されています。01はEscapeで閉じられますが、キーボードだけで項目間を移動する手段はなく、03・04・05は現時点でフォーカスを受け取るだけで専用のキーハンドラがなく、値を変えるにはドラッグしかありません。aria-liveはこの9項目のどこにも使われておらず、状態が支援技術に届くのは09のaria-valuenow・aria-valuetextの組と01のaria-expanded・aria-hiddenの組を通じてだけなので、スクリーンリーダーの利用者にとって最も手がかりが明確なのはこの2項目です。

FAQ

css ゲージ デザインとして06以外の項目も使えますか

使えます。06は単純な割合表示として最もそのままの形ですが、値を角度に変換する計算そのものは09にも同じ形で現れます。09はキーボード操作を前提にしているため、フォーカス表示とaria-valuenowが必要な設定画面のような場面では、06よりも09の構造の方が近い出発点になります。

なぜ04・05・07だけ、手を離した後も動き続けるのですか

この3項目は状態を最後のポインタ位置ではなく物理ループ(減衰・スナップと弾み・スプリング)の中に持っているためです。04は純粋な運動量のシミュレーションで、05と07はそれぞれ違う曲線で特定の目標値へ向かいます。02・06・09のようにこの挙動を持たない項目は、操作が終わった時点の値をそのまま保持し、ループが解決すべきものが残っていません。

プログレスバー css アニメーションとして最も計算が重いのはどれですか

動いたフレーム数で見ると02・06・08が23フレーム中23フレームで並び、常に回転中のホイール・毎ティック塗り直されるゲージ・色と位置が連動するスライダーは、ほぼ常に新しく計算することを抱えています。平均強度で見ると06が181.2で最も高く、GAUGE_STEP_DEGの輪郭が立った段差はなめらかなトランジションより1回あたりのピクセル差が大きくなるためです。とはいえ9項目のどれも計算自体は軽く、ワーカーを使わずメインスレッドだけで十分動きます。

Sources: MDN — conic-gradient() · MDN — Pointer events · MDN — menu role · MDN — Web Animations API

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

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