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

星評価 UI 9選 — 塗りの値が状態に応じて変わる仕組み

9つのうち1つは指が動いた横方向の値に沿って塗りの値がすぐ変わり、1つは平均値の小数点部分が星1つの塗り幅の値として重なって現れ、もう1つは同じ星がもう一度押された状態になると塗りの値が0に向けて弾むように減ります。星評価 ui を9パターン分けて実装しました。

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

9つのどれも実際のサーバーには触れていません。画面に見える評価や件数はすべてスクリプトの中で宣言されたローカルな数値で、fetch の呼び出しはどこにもありません。9つとも自動再生だけで動くのではなく、必ず本物のユーザー入力に応答します。2個はホバー(pointerenter・pointerleave)に、2個はポインター移動(pointermove、うち1個は Pointer Events によるドラッグ捕捉つき)に、1個は矢印キーの keydown に、4個は click に応答します — 2足す2足す1足す4で、9つとも実際のトリガーです。

9つのパターンの解剖 — どの値がいつ変わるか

9つは、入力が実際にどの値を書き換えるかで分かれます。ホバーされた順番の値か、横方向の座標の値か、キーボードの目標値か、確定する評価の値か — 評価 星 デザインの集まりが「同じ5つの星が9回並んでいるだけ」に見えずに済むのは、この分かれ方があるためです。

なぜ2つは確定値ではなく先読みの値を書くのか

01と06はどちらも pointerenter を聞いていますが、入っただけでは何も確定しません。01の previewTo() は指している星の順番の値までの一時的な塗りを描くだけで、revertToValue() が pointerleave のときに確定済みの値へ戻します。06の requestSwap() はさらにその先を行き、ラベルの切替自体を40msの setTimeout の奥に置いています。列を素早く横切るカーソルにはふきだしが一切出ず、遅延を超えて留まったホバーだけが commit() を得ます。どちらも、クリックか離脱が最終状態を決めます。

なぜ2つは位置の値そのものを評価に変えるのか

02と09はどちらも生の横座標を読みますが、解像度が正反対です。02の valueFromX() は各星の getBoundingClientRect() を順に調べ、ポインターが幅のどちら半分にあるかで整数か半星かに値をスナップし、paint() がその値を星ごとの塗り幅へ変換します。09の setPos() は代わりにトラック上の任意のピクセル位置を受け取り、小数点以下1桁の連続した0〜5の値へ変換して、指が押されている間は pointermove のたびに塗り直し続けます — 指を離すまでスナップはありません。

なぜ5つは1つのイベントで離散的な値を確定するのか

03・04・05・07・08は、どれも1回の操作で状態をちょうど1回変えます。03は矢印キー1回ごとに target の値を1動かし、tick() が表示値をそこへ滑らかに寄せます。04の addReview() は待ち行列の次の評価を counts に1つ加えて加重平均を計算し直します。05はクリックされた行の件数を全体に対する比率へ変えます。07はクリックされた順番の値を mix() に通して色を作ります。08はクリックされた星の順番の値を現在の値と比べ、一致すれば0へ初期化します。1つの入力が入り、1つの塗られた状態が出ます。

パターン別コード

01ホバー先読み塗り — 指している星の順番の値まで塗りの状態が先に見え、外れると元の値に戻る方式

ポインターが星の上に置かれた状態になるとその星までの順番の値だけ塗りの状態が先に現れ、ポインターが外れた状態になると塗りの値は実際に選ばれた値に戻ります。商品のレビューフォームやレシピカードはこの形で、クリックが確定する前にその結果を先に見せます。

ホバー先読み塗り — 指している星の順番の値まで塗りの状態が先に見え、外れると元の値に戻る方式
新しいタブで開く

01 · ホバー先読み塗り — 指している星の順番の値まで塗りの状態が先に見え、外れると元の値に戻る方式 — 計測 累積面積 3.944% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ratioOf(n)previewTo(i) / revertToValue()value = 3

数字が語ること

23フレームの追跡のうち、01は21フレームで動き、累積面積3.94%・平均強度75.6を記録しました — フレーム覆盖率がほぼ最大なのに面積が最小なのは、自動再生ループが先読みの値を列の端から端まで動かすため、常に何かが動いていながら、5つ並んだ星の行は480×300のステージのごく一部しか占めないためです。

  // 計算の層: 塗りの個数 n を星の総数で割った比率(%)をそのまま画面の数値に
  // 使う — 塗りはこの計算から出た整数で、参照ではない。
  function ratioOf(n) { return Math.round((n / TOTAL) * 100); }

  function paint(n, isPreview) {
    stars.forEach(function (s, idx) {
      s.classList.toggle('sr__star--filled', idx < n);
    });
    numEl.textContent = String(n);
    pctEl.textContent = String(ratioOf(n));
    readout.classList.toggle('sr__readout--preview', !!isPreview);
    readout.firstChild.textContent = isPreview ? 'プレビュー ' : '選択 ';
  }

02半星精密塗り — 星1つ分の幅の値のどちら半分にポインターの横位置の値があるかで塗りの値が整数か半星の値に分かれる方式

ポインターの横位置の値が星1つ分の幅の値のどちらの半分にあるかによって塗りの値が整数の値か半星の値のどちらかに決まります。3.5が意味を持つホテルの詳細ページや書評ウィジェットが、この精度を使います。

半星精密塗り — 星1つ分の幅の値のどちら半分にポインターの横位置の値があるかで塗りの値が整数か半星の値に分かれる方式
新しいタブで開く

02 · 半星精密塗り — 星1つ分の幅の値のどちら半分にポインターの横位置の値があるかで塗りの値が整数か半星の値に分かれる方式 — 計測 累積面積 4.027% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

valueFromX(clientX)STAR_W = 44steps = [0.5, 1, 1.5, …, 5]

数字が語ること

23フレームの追跡のうち、02は全23フレームで動き、累積面積4.03%・平均強度153.2を記録しました — 被覆率が満杯で強度は9つの中で2番目に高いのは、塗りの端が星の幅のどこにでも置ける(境界にしか置けないのではなく)ため、ポインターが動いたぶんだけ毎回切り取り幅が塗り直されるためです。

  // 計算の層: 各星の実際の描画幅(getBoundingClientRect)を基準に、ポインターが
  // 左半分と右半分のどちらにあるかを分け、値は整数か半星かの単位にだけスナップする。
  function valueFromX(clientX) {
    for (var i = 0; i < stars.length; i++) {
      var r = stars[i].getBoundingClientRect();
      if (clientX < r.left) return i;
      if (clientX <= r.right) {
        var half = r.left + r.width / 2;
        return clientX < half ? i + 0.5 : i + 1;
      }
    }
    return stars.length;
  }

  function paint(v) {
    stars.forEach(function (s, idx) {
      var starVal = Math.max(0, Math.min(1, v - idx));
      clips[idx].style.width = (starVal * 100).toFixed(0) + '%';
    });
    numEl.textContent = v.toFixed(1);
  }

03矢印キー値移動 — 押された方向の値だけ選ばれた星の順番の値が1つ増減する方式

右か左の矢印キーが押されるたびに選ばれた星の順番の値が1つ増減し、その値が範囲の両端を超えた状態になると端の値で止まります。マウスに頼れない精算フローは、この方式だけで完結します。

矢印キー値移動 — 押された方向の値だけ選ばれた星の順番の値が1つ増減する方式
新しいタブで開く

03 · 矢印キー値移動 — 押された方向の値だけ選ばれた星の順番の値が1つ増減する方式 — 計測 累積面積 3.694% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

tick() / animateTo(t)display += (target - display) * 0.2seq = [4, 5, 3, 1, 0, 2, 3]

数字が語ること

23フレームの追跡のうち、03は16フレームで動き、累積面積3.69%・平均強度127.6を記録しました — キーを押したあとに1回の跳躍ではなく連続した動きフレームが続くのは、tick() が表示値を目標に追いつくまで数フレームにわたって動かし続けるためで、イージングがフレーム数として現れています。

  // 計算の層: 毎フレーム、表示値を目標の値へ向かって20% ずつだけ動かす線形補間(lerp) —
  // 値は飛ばず、曲線を描いて追いつく。
  function tick() {
    display += (target - display) * 0.2;
    if (Math.abs(target - display) < 0.01) {
      display = target;
      paint(display);
      raf = null;
      return;
    }
    paint(display);
    raf = requestAnimationFrame(tick);
  }
  function animateTo(t) {
    target = Math.max(0, Math.min(TOTAL, t));
    row.setAttribute('aria-valuenow', String(target));
    numEl.textContent = String(target);
    if (raf === null) raf = requestAnimationFrame(tick);
  }

04平均値重ね塗り — 平均値の小数点部分が星1つ分の塗り幅の値として重なって現れる方式

平均値の整数部分の分だけ星が塗られた状態になり、残った小数点部分の値が次の星1つの塗り幅の値として重なって現れます。十分な人数が評価を終えたレビュー概要が到達する、読み取り専用の最終状態です。ストアの一覧やアプリの詳細ページに映るのはまさにこの形です。

平均値重ね塗り — 平均値の小数点部分が星1つ分の塗り幅の値として重なって現れる方式
新しいタブで開く

04 · 平均値重ね塗り — 平均値の小数点部分が星1つ分の塗り幅の値として重なって現れる方式 — 計測 累積面積 0.5% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

average()counts = {5:12, 4:8, 3:3, 2:1, 1:1}queue = [5,3,5,4,2,5,4,3,5,1]

数字が語ること

23フレームの追跡のうち、04は18フレームで動き、累積面積0.50%・平均強度73.1を記録しました — 面積は9つの中で断トツに小さいのは、レビューが1件増えるたびに平均の動きは小さくなり、4.1台に乗った棒は星1つの切り取り幅の薄い一部を塗り直すだけで、ほかはほとんど変わらないためです。

  // 計算の層: 点数ごとの件数(counts)から加重平均(合計 / 個数)を求め、整数部分の分だけ
  // 星を塗り、残った小数点部分を次の星1つの塗り幅の値として重ねて描く。
  var counts = { 5: 12, 4: 8, 3: 3, 2: 1, 1: 1 };
  var queue = [5, 3, 5, 4, 2, 5, 4, 3, 5, 1];
  var qi = 0;

  function average() {
    var sum = 0, n = 0;
    for (var k in counts) { sum += Number(k) * counts[k]; n += counts[k]; }
    return { avg: sum / n, n: n };
  }

  function paint() {
    var r = average();
    clips.forEach(function (el, idx) {
      var starVal = Math.max(0, Math.min(1, r.avg - idx));
      el.style.width = (starVal * 100).toFixed(1) + '%';
    });
    avgEl.textContent = r.avg.toFixed(2);
    nEl.textContent = String(r.n);
  }

05件数バー分解 — 星の点数ごとの回答件数の値が横棒の長さの値に分かれて現れる方式

星5から1までの各点数の回答件数の値が全体の回答件数の値に対する比率の値に変わり、その行の棒の長さの値になります。商品ページの評価分布ブロックは、平均値の横にこの行の並びと同じ形で置かれます。

件数バー分解 — 星の点数ごとの回答件数の値が横棒の長さの値に分かれて現れる方式
新しいタブで開く

05 · 件数バー分解 — 星の点数ごとの回答件数の値が横棒の長さの値に分かれて現れる方式 — 計測 累積面積 22.699% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

total() / paint()counts = {5:42, 4:21, 3:9, 2:4, 1:3}bump = [5,3,4,5,2,5,1,4,5,3]

数字が語ること

23フレームの追跡のうち、05は15フレームで動き、累積面積22.70%・平均強度34.0を記録しました — 面積は9つの中で抜けて大きいのは、ステージの幅いっぱいに広がった5本の棒がすべて塗り直しのたびに伸びるためです。その代わり強度は最も低く、同じイベントが少数の画素を鋭くではなく、多数の画素を穏やかに変えています。

  var counts = { 5: 42, 4: 21, 3: 9, 2: 4, 1: 3 };
  var selected = null;

  // 計算の層: 各点数の回答件数を回答件数の合計で割った比率(%)を、その行の棒の長さの値に使う。
  function total() {
    var t = 0;
    for (var k in counts) t += counts[k];
    return t;
  }

  function paint() {
    var t = total();
    rows.forEach(function (row) {
      var star = Number(row.getAttribute('data-star'));
      var pct = t ? (counts[star] / t) * 100 : 0;
      row.querySelector('[data-bar]').style.width = pct.toFixed(1) + '%';
      row.querySelector('[data-pct]').textContent = Math.round(pct) + '% (' + counts[star] + ')';
      row.classList.toggle('sr__row--on', star === selected);
      row.setAttribute('aria-pressed', String(star === selected));
    });
  }

06ふきだしラベル切替 — 指している星の順番の値に合わせてラベル文言の値が変わって現れる方式

指している星の順番の値が変わるたびにその値に対応するラベル文言の値が一緒に変わって星の横に現れ、外れた状態になるとラベルは消えます。裸の3や5ではなく「普通」「最高」と言う評価の入力欄は、数値に意味をくっつけたままにします。

ふきだしラベル切替 — 指している星の順番の値に合わせてラベル文言の値が変わって現れる方式
新しいタブで開く

06 · ふきだしラベル切替 — 指している星の順番の値に合わせてラベル文言の値が変わって現れる方式 — 計測 累積面積 6.522% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LABELS = {1:'매우 별로', …, 5:'최고'}positionTip(i) — getBoundingClientRect 중심 xrequestSwap(i) — 40ms debounce

数字が語ること

23フレームの追跡のうち、06は9フレームで動き、累積面積6.52%・平均強度143.6を記録しました — 動くフレームは少ないのに1回ごとは鋭いのは、40msのデバウンスがカーソルが横切っただけの星をすべて飛ばし、ふきだしが実際に確定した星だけが塗り直されるためです。ふきだしのポップ自体がコントラストの高い変化です。

  // 計算の層: 星の実際の描画位置(getBoundingClientRect の中心 x)をふきだしの translateX の値に
  // そのまま移し、インデックスの変更は短い遅延(デバウンス)を通したあとにだけラベル文言を
  // 替える — 素早く横切るときに文言が星ごとに点滅しないようにするタイミングの計算である。
  function positionTip(i) {
    var star = stars[i - 1];
    var sr = star.getBoundingClientRect();
    var rr = row.getBoundingClientRect();
    var cx = sr.left + sr.width / 2 - rr.left;
    tip.style.transform = 'translateX(' + cx.toFixed(1) + 'px) translateX(-50%)';
  }

  function commit(i) {
    shownIndex = i;
    tip.textContent = LABELS[i];
    positionTip(i);
    tip.classList.add('sr__tip--show');
    stars.forEach(function (s, idx) { s.classList.toggle('sr__star--filled', idx < i); });
  }

  function requestSwap(i) {
    if (pendingTimer) clearTimeout(pendingTimer);
    pendingTimer = setTimeout(function () { commit(i); }, 40);
  }

07値別色変化 — 選ばれた値の大きさに応じて星の色の値が段階的に変わる方式

選ばれた星の順番の値が低い範囲にあると色の値が一方に、高い範囲にあると別の色の値に変わり、その間の値は2色の間を連続してつながります。満足度調査やサービス評価では、個数だけでなく色のほうに温度を持たせられます。

値別色変化 — 選ばれた値の大きさに応じて星の色の値が段階的に変わる方式
新しいタブで開く

07 · 値別色変化 — 選ばれた値の大きさに応じて星の色の値が段階的に変わる方式 — 計測 累積面積 3.591% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

mix(t) — RGB 채널 선형보간LOW = [0xc2, 0x36, 0x1a]HIGH = [0x4c, 0xd4, 0xa6]

数字が語ること

23フレームの追跡のうち、07は全23フレームで動き、累積面積3.59%・平均強度25.7を記録しました — 強度は9つの中で最も低いのは、2つの固定された端点の間の色の変化が1ステップごとに各画素をわずかにしか変えないためです。位置や幅の変化が全コントラストで端を描き直すのとは対照的です。

  // 計算の層: 選ばれた値(1〜5)を0〜1の比率 t に変え、2つのトークン色の R・G・B チャンネルを
  // それぞれ t だけ線形補間(mix)して中間色を作る — HSL の回転ではなく、チャンネルごとの
  // 算術的な混色である。
  var LOW = [0xc2, 0x36, 0x1a];
  var HIGH = [0x4c, 0xd4, 0xa6];

  function mix(t) {
    var c = LOW.map(function (v, i) { return Math.round(v + (HIGH[i] - v) * t); });
    return 'rgb(' + c.join(',') + ')';
  }

  function paint(value) {
    var t = (value - 1) / 4;
    var color = mix(t);
    stars.forEach(function (s, idx) {
      s.style.color = idx < value ? color : '';
      s.classList.toggle('sr__star--filled', idx < value);
    });
    numEl.textContent = String(value);
    readout.style.color = color;
  }

08戻しバネ初期化 — 同じ星がもう一度押された状態になると塗りの値が0に向けて弾むように減る方式

すでに選ばれた値と同じ星がもう一度押された状態になると塗りの値が0に向かって弾む動きの値を経て減り、選択の値も一緒に消えます。別の「クリア」ボタンを置かずに、初期化の操作を星の並びの中に組み込む形です。

戻しバネ初期化 — 同じ星がもう一度押された状態になると塗りの値が0に向けて弾むように減る方式
新しいタブで開く

08 · 戻しバネ初期化 — 同じ星がもう一度押された状態になると塗りの値が0に向けて弾むように減る方式 — 計測 累積面積 4.791% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

reset() — wasFilled 필터srBounce — scale(1)→scale(1.3)→scale(.85)value = 4 (초기값)

数字が語ること

23フレームの追跡のうち、08は9フレームで動き、累積面積4.79%・平均強度108.7を記録しました — 動きが連続ではなく突発的なのは、バネのキーフレームが初期化の瞬間にだけ発火し、サイクルの残りは静止しているためです。その代わり、塗られた星の行全体のスケール兼フェードは、乗ったときの変化として強いものです。

  // 計算の層: 初期化が起こると塗りの値を0へ送ると同時に、すでに塗られていた星にだけ
  // バネのイージング(退場前のオーバーシュート)のついたポップアニメーションのクラスを
  // 重ねる。
  function reset() {
    var wasFilled = stars.filter(function (s, idx) { return idx < value; });
    value = 0;
    paint(value);
    wasFilled.forEach(function (s) { s.classList.add('sr__star--bounce'); });
    setTimeout(function () {
      wasFilled.forEach(function (s) { s.classList.remove('sr__star--bounce'); });
    }, 620);
  }

09タッチドラッグ操作 — 指が動いた横方向の値に沿って塗りの値がリアルタイムで変わる方式

指が星の列の上を押したまま動いている間、その横位置の値が変わるたびに塗りの値が同じ画面上ですぐ追従して変わり、指を離した状態になるとその値が選択の値として固定されます。親指1本で進めるモバイルの評価ステップは、5つの別々のタップ目標よりもこちらを選びます。

タッチドラッグ操作 — 指が動いた横方向の値に沿って塗りの値がリアルタイムで変わる方式
新しいタブで開く

09 · タッチドラッグ操作 — 指が動いた横方向の値に沿って塗りの値がリアルタイムで変わる方式 — 計測 累積面積 4.96% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

runInertia() — velocity *= 0.945Math.abs(velocity) > 0.015onDown/onMove — pointerId capture

数字が語ること

23フレームの追跡のうち、09は17フレームで動き、累積面積4.96%・平均強度173.2を記録しました — 強度は9つの中で最も高いのは、指を離したあとも慣性が値を流し続け、その1ステップごとに塗りの端とトラックのつまみが全コントラストで一緒に塗り直されるためです。面積ではなく精度で測れば、表示値が入力に最も忠実に追う方式です。

  function onUp() {
    if (!dragging) return;
    dragging = false;
    runInertia();
  }
  // 計算の層: 指を離した瞬間の速度(px/ms)をそのまま引き継ぎ、毎フレーム減衰(摩擦)させながら
  // 位置を動かし続ける慣性スクラブ — 離した瞬間に値が止まらず、流れてから止まる。
  function runInertia() {
    velocity *= 0.945;
    setPos(pos + velocity * 16);
    if (Math.abs(velocity) > 0.015 && pos > 0 && pos < width()) {
      inertiaRaf = requestAnimationFrame(runInertia);
    } else {
      inertiaRaf = null;
    }
  }

値がパターンを変える場所 — 位置・比率・時間の境界

なぜ03番の lerp 係数は値の着地地点ではなく道のりを決めるのか

display += (target - display) * 0.2 の 0.2 は、目標そのものを一切変えません — animateTo() のクランプはどちらの値でも同じ0〜5の範囲のままです。0.2では表示値が数フレームをかけて目標へ滑らか寄せていくため、方向の反転が曲線として見えます。0.4では同じ矢印キー1回で値がほぼ1フレームで届き、即応に感じられる代わりに、動きの慣性は一瞬にかわります。着地点はどちらでも同じです。

なぜ06番の40msのデバウンスはふきだしの見た目ではなく存在そのものを決めるのか

遅延は requestSwap() の中だけにあります — 40ms 未満のホバーは、次の clearTimeout によって commit() が走る前に取り消されるため、そのふきだしは最初から存在しなかったことになります。90msではさらに多くのかすりが消え、その代わり、意図して留まったホバーも1拍長く待つことになります。ふきだしの位置の計算(positionTip())も文言も、この数値には触れられません。

なぜ09番の摩擦0.945は止まる位置ではなく流れる距離を決めるのか

velocity *= 0.945 は、離した瞬間に残っていた速度に毎フレーム掛かる値であって、指が実際に到達した位置を書き換えるものではありません — 決めるのは、その先に値がどれだけ流れるかだけです。0.975では同じ離し方でもはるかに長く滑り、確定する値は指を離した場所よりだいぶ先に着きます。0.945では流れは短く尽き、値は離した位置の近くで収まります。

どこで壊れるか — 連続クリック・狭い画面・タッチとマウスが混ざるとき

バネがまだ動いているうちに連続クリックすると何が起こるか

08の reset() は620ms後に sr__star--bounce を外す setTimeout を予定しますが、新しいクリックが先に星を塗り直しても何もこれを取り消しません — 待機中のタイマーは塗りの途中で発火し、塗られているはずの星からクラスを剥がして、バネを途中で断ち切ります。09は自分のアニメーションの世話をちゃんとしています。onDown() は新しいドラッグが始まる前に待機中の inertiaRaf を取り消すため、連続クリックで慣性ループが同じ値に2重に積まれることはありません。

行が狭い画面の端に来ると何が起こるか

9つとも @media (max-width: 340px) のブロックで星を縮め、02・06・09はイベントの時点で getBoundingClientRect() を測るため、値の計算はキャッシュした幅を信じることなく自動で追従します — 例外は06のふきだしです。その translateX の位置は行の両端に対してクランプされていないため、狭い画面では、ラベルが端の星の上に留まらずに行の外側へはみ出すことがあります。

タッチとマウスの入力が同じ要素に混ざると何が起こるか

01と06はホバーで動く(pointerenter・pointerleave)ため、タッチ画面で星をタップした指は、余分な enter と leave の組を1回発火させ、2回目のタップでしか消えない先読みやふきだしの状態を残すことがあります。02は行の pointermove を setPointerCapture なしで聞いているため、指が行から外れたタッチドラッグは操作の途中で更新を止めます。09はこれを意図的に扱っています。onDown() の track.setPointerCapture(e.pointerId) が、指が離れるまでのすべての移動をトラックへ届け続けます — まさに Pointer Events がそのために存在する統一的な扱いです。

このセットが CSS ハート アニメーション 9選 — 一行でコピペOK・セグメントコントロールUI 9選 — コピペOK・タイムピッカー ui 9選 — 一点から区間まで と違うのは、操作がどの値を書くかです。ハートのセットは「いいね」ボタン自身の状態変化を動かすため、値は入か切の2値だけで、工夫はすべてイージングにあります。セグメントコントロールのセットは固定された選択肢の行を1つの選択が横切る — 形はこのセットに最も近いですが、位置の値は区分の所有物であって、小数の塗りには決してなりません。タイムピッカーのセットは点や入力を区間の値に変えるため、評価が1つで済むところに、値は両端を持っています。この星評価 デザインのセットのソースはすべて、パスワードhaq93vzhで解除すると、デモの自動再生ループを外すだけで実際のレビューフォームにそのまま組み込めます。

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

9つのうち、出荷するバリエーションとして定数を公開する価値があるのは3つだけです — ほかの6つに調整できる数値がないのではなく、lerp 係数・デバウンスの遅延・摩擦の掛け算の3つは、1つの値を変えるだけで操作の大目さ・慎重さ・なめらかさが最も目に見えて変わるためです。

バリエーション 何が変わるか トレードオフ
03の lerp 係数 0.2 → 0.4 表示値が矢印キーの目標に追いつく速さ 0.2では反転が見えるイージングの曲線を描き、0.4では値がほぼ即時に届いて機敏に感じられる代わりに、動きに物理感を与えていた慣性が短くなります
06の切替遅延 40ms → 90ms どのホバーがふきだしのラベルを替えるほど長いか 40msでは星を素早くかすってもラベルが出て、90msではかすりは黙ったままですが、意図して留まったホバーも1拍長く待つことになります
09の慣性摩擦 0.945 → 0.975 指を離したあと値がどれだけ流れるか 0.945では離した直後に流れが尽きて値は離した位置の近くで収まり、0.975では同じ離し方がはるかに先まで滑り、なめらかだが指が触れていない値で確定します

アクセシビリティは、このデモの作りとしては一様ではありません。どこまで整っているかは、ソースが正確に語っています。01が最も完備しています。行に aria-label つきの role="radiogroup"、星ごとに role="radio" と aria-checked を置き、commit() の中で同期を取っています。03がその次です。role="slider" に tabindex="0"・aria-valuemin/aria-valuemax を備え、animateTo() のたびに aria-valuenow を書き込みます。05は role="list" に行ごとの role="listitem" と aria-pressed を持っています。残る6つ — 02・04・06・07・08・09 — は ARIA のロールも状態も一切設定しておらず、視覚的な状態だけを描いています。各項目の @media (prefers-reduced-motion: reduce) は CSS トランジションを切り(08はさらにバウンドアニメーションも0にします)、各スクリプト自身の reduce チェックが止めるのは自動再生の setInterval だけです — 実際の入力はどちらの設定でもまったく同じに動きます。

FAQ

css 星評価のパーセント表示に JavaScript は必須ですか

01のパーセント表示は ratioOf() — Math.round((n / TOTAL) * 100) — から出ていて、塗りと同じ個数の値から作られるため、常に一致します。固定幅を直接書けば css だけのバーも作れますが、入力に応じて値が変わる瞬間から再計算する何かが必要になり、ここではそれが4行のスクリプトです。

半星の方式とドラッグの方式は同じ値の目盛りを共有できますか

すでに共有しています — 02は valueFromX() で0.5刻みにスナップし、09は paintFromPos() で小数点以下1桁を保ち、どちらも同じ Math.max/Math.min の形で0〜5の中に収めています。違うのは範囲ではなく解像度です。09の連続した目盛りを02の行に持ち込むことは、ドラッグ位置を近いほうの半星に丸めることと同じことです。

この9つは評価をどこかに送信しますか

いいえ。評価も件数も平均もスクリプトの中で宣言された数値で、04の「レビュー追加」ボタンはローカルの queue 配列の次の値を counts に加えるだけで、9つのどれにも fetch・XHR・フォーム送信は存在しません。javascript 星評価 表示としてこの形を使う場合は、貼り付けたあとに実際の送信先を自分で組み込む前提です。

Sources: MDN — Element.getBoundingClientRect() · MDN — Pointer events · MDN — requestAnimationFrame() · MDN — prefers-reduced-motion · MDN — ARIA slider role

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

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