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

ランキング デザイン css 9パターン — 順位表 デザインも同じ仕組み

「ランキング デザイン css」の9パターンはどれも、1位が入れ替わる瞬間の見え方をひとつの計算式に絞り込んでいます。ランナーがゴール直前で位置を入れ替える瞬間のように、順位表もそのたびに違う形で揺れる —

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

どの項目も LEADERBOARD_FLIP_DURATION_MS や LEADERBOARD_SCROLL_FRICTION のような、たったひとつの定数が計算のもとになっています。ライブラリは一切使わず、値を変えるだけで動きの性格が変わる仕組みです。

ランキング デザイン web で実際に効く9つの使いどころ

01番と04番は、リアルタイム対戦ゲームの順位が入れ替わる瞬間そのもののように「誰が誰を抜いたか」自体を見せたい場面に向いています — 01番は入れ替わりそのものを、04番は数字が変わる前から影の高さで順位を感じさせます。02番の弾みはポイント制の報酬画面で「順位が上がった」という手応えを作り、03番の桁ロールは順位よりも数値そのものを見せたいライブスコアのダッシュボードに自然に収まります。

05番は殿堂入りページのように1位だけを特別扱いしたい場面向けで、08番の比例バーはスコアの相対差そのものを見せたい表彰・結果画面に合います。06番の慣性スクロールは画面の狭い端末で長いランキングを見せるために、矢印キーや Page Up/Page Down にも対応しています。07番の点滅矢印は変化量の大きさが方向と同じくらい重要な週次レポート向けで、09番の同色点滅はクイズや大会の結果で本当に同点が起こる場面のために作られています。ランキング デザイン おしゃれに寄せたいだけの場面なら05番や08番、実際の順位計算の正確さを優先するなら01番や09番が土台になります。

01行同士がすれ違うように移動するFLIP順位交換

スコアが入れ替わると、flipSwap() が並び替え前後それぞれの行の位置を測り、animateFlipRow() がその差分を開始位置とする translateY() を用意して LEADERBOARD_FLIP_DURATION_MS の間にゼロへ戻します。CSS トランジションではなく Web Animations API の el.animate() を使うのは、document.getAnimations() で捕捉できる形にしてデモの撮影タイミングを毎回同じにするためで、同じ行がすぐにまた動かされた場合は node.anim.cancel() が前の再生を打ち切ってから新しいアニメーションに切り替えます。

FLIPで順位が入れ替わるカード
新しいタブで開く

01 · FLIPで順位が入れ替わるカード — 計測 累積面積 33.704% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LEADERBOARD_FLIP_DURATION_MSmeasureRectsflipSwap
  function animateFlipRow(node, dy) {
    var li = node.li;
    if (node.anim) node.anim.cancel();
    var baseShadow = "0 2px 10px rgba(10,16,32,0)";
    var peakShadow = "0 " + (2 + 12).toFixed(1) + "px " + (10 + 20).toFixed(1) + "px rgba(10,16,32,.28)";
    var baseBg = "rgba(" + LEADERBOARD_FLIP_HIGHLIGHT + ",0.08)";
    var peakBg = "rgba(" + LEADERBOARD_FLIP_HIGHLIGHT + ",0.32)";
    node.anim = li.animate([
      { transform: "translateY(" + dy + "px)", boxShadow: baseShadow, background: baseBg, offset: 0 },
      { boxShadow: peakShadow, background: peakBg, offset: 0.5 },
      { transform: "translateY(0px)", boxShadow: baseShadow, background: baseBg, offset: 1 }
    ], { duration: LEADERBOARD_FLIP_DURATION_MS, easing: "cubic-bezier(.2,.8,.2,1)", fill: "forwards" });
    node.anim.onfinish = function () {
      li.style.transform = "";
      li.style.boxShadow = "";
      li.style.background = "";
    };
  }

測定値: 面積変化33.7%、強度29.9、動いたフレームは7/23です。

02順位が上がると弾んで大きくなるスプリング

順位が上がると、springScale() が LEADERBOARD_RANKUP_OVERSHOOT_PERCENT の値からダンピング比 zeta を逆算し、二次遅れ系のステップ応答の閉形式で減衰振動の曲線を作ります。popRow() はその曲線を独自の requestAnimationFrame() ループの中で毎フレーム評価するだけで、物理シミュレーションを1ステップずつ進めているわけではありません。オーバーシュートの値を大きくするほど、行が1倍を大きく超えてから戻ってくる幅が広がります。

順位が上がるとスプリングで弾む行
新しいタブで開く

02 · 順位が上がるとスプリングで弾む行 — 計測 累積面積 23.776% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LEADERBOARD_RANKUP_OVERSHOOT_PERCENTpopRowLEADERBOARD_RANKUP_POP_MS
  function springScale(t, overshootPercent) {
    var os = Math.max(0.001, Math.min(0.9, overshootPercent / 100));
    var lnOs = Math.log(os);
    var zeta = -lnOs / Math.sqrt(Math.PI * Math.PI + lnOs * lnOs);
    var wn = 11.5;
    var wd = wn * Math.sqrt(Math.max(0.0001, 1 - zeta * zeta));
    var tt = Math.min(1, t) * (LEADERBOARD_RANKUP_POP_MS / 1000);
    return 1 - Math.exp(-zeta * wn * tt) * (Math.cos(wd * tt) + (zeta * wn / wd) * Math.sin(wd * tt));
  }

測定値: 面積変化23.8%、強度160.4、動いたフレームは22/23です。

03一桁ずつ順番に止まるスコアカウンター

rollTo() はスコアを3桁に分解し、各桁の transitionDelay を桁の位置 × LEADERBOARD_DIGIT_ROLL_STAGGER_MS だけずらして設定するので、百の位・十の位・一の位が同時にではなく順番に止まります。各桁はそれぞれ digit × LEADERBOARD_DIGIT_H 分の translateY() で表現され、実際に値が変わった桁だけに is-rolling クラスが付くため、780から781への変化では1マスだけ、891への変化では3マスすべてが光ります(830では一の位が0のまま変わらないため、光るのは2マスです)。

スコアの数字が一桁ずつ回転するカウンター
新しいタブで開く

03 · スコアの数字が一桁ずつ回転するカウンター — 計測 累積面積 41.486% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LEADERBOARD_DIGIT_ROLL_STAGGER_MSbuildReelLEADERBOARD_DIGIT_H
  function rollTo(row, score) {
    var str = String(score);
    while (str.length < 3) str = "0" + str;
    var changed = false;
    for (var i = 0; i < 3; i++) {
      var digit = parseInt(str[i], 10);
      var delay = reduce ? 0 : i * LEADERBOARD_DIGIT_ROLL_STAGGER_MS;
      var dur = reduce ? 0 : LEADERBOARD_DIGIT_ROLL_MS;
      var reel = row.reels[i];
      var box = row.digitBoxes[i];
      reel.style.transitionDelay = delay + "ms";
      reel.style.transitionDuration = dur + "ms";
      reel.style.transform = "translateY(-" + (digit * LEADERBOARD_DIGIT_H) + "px)";
      if (!reduce && row.prevDigits[i] !== digit) {
        changed = true;
        box.style.setProperty("--roll-delay", delay + "ms");
        box.style.setProperty("--roll-ms", dur + "ms");
        box.classList.remove("is-rolling");
        void box.offsetWidth;
        box.classList.add("is-rolling");
        window.setTimeout(function (b) { return function () { b.classList.remove("is-rolling"); }; }(box), delay + dur + 60);
      }
      row.prevDigits[i] = digit;
    }

測定値: 面積変化41.5%、強度68.6、動いたフレームは8/23です。

04順位が高いほど影が浮かび上がる行

elevationForX() は0から1に正規化した値 x ひとつから、影のぼかし・オフセット・不透明度、そしてカードの浮き上がり量までをまとめて計算します。それぞれ LEADERBOARD_ELEVATION_BLUR_MAX_PX や LEADERBOARD_ELEVATION_LIFT_MAX_PX など、同じ x に対する係数として上限が決まっている点は共通です。順位が変わって x が変化すると、tweenRow() が --lbe-duration という CSS カスタムプロパティから読み取った時間ぶんだけ requestAnimationFrame() で x を補間するので、並び替えの瞬間に影が瞬間移動することがありません。

順位が高いほど影が浮き上がる行
新しいタブで開く

04 · 順位が高いほど影が浮き上がる行 — 計測 累積面積 21.265% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

elevationForLEADERBOARD_ELEVATION_BLUR_MAX_PXapplyElevation
  function elevationForX(x) {
    return {
      blur: 4 + x * LEADERBOARD_ELEVATION_BLUR_MAX_PX,
      offset: 2 + x * LEADERBOARD_ELEVATION_OFFSET_MAX_PX,
      alpha: 0.08 + x * LEADERBOARD_ELEVATION_ALPHA_MAX,
      lift: x * LEADERBOARD_ELEVATION_LIFT_MAX_PX
    };
  }

  function paintElevation(row, e) {
    row.li.style.boxShadow = "0 " + e.offset.toFixed(1) + "px " + e.blur.toFixed(1) + "px rgba(23,20,26," + e.alpha.toFixed(2) + ")";
    row.li.style.transform = "translateY(-" + e.lift.toFixed(1) + "px)";
  }

  function applyElevation(row, rank) {
    paintElevation(row, elevationFor(rank));
  }

測定値: 面積変化21.3%、強度60.6、動いたフレームは11/23です。

051位の行だけを流れる金色の光沢

sweepFrame() は毎フレーム1位と2位のスコア差を0から1の比率として測り直し、その比率をそのまま LEADERBOARD_GOLD_SWEEP_FAST_MS と LEADERBOARD_GOLD_SWEEP_SLOW_MS の間の周期に線形補間します — 僅差なら光の帯が速く流れ、差が開くほどゆっくり流れます。smoothstep() が帯の位置を滑らかにイージングし、同じ位相を使った Math.sin() の山が通過の中央だけで1位の行の背景と box-shadow を明るくします。

1位の行だけを通り過ぎる金色の光沢
新しいタブで開く

05 · 1位の行だけを通り過ぎる金色の光沢 — 計測 累積面積 12.883% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LEADERBOARD_GOLD_SWEEP_FAST_MSsmoothstepsweepFrame
  function sweepFrame(now) {
    var order = sortedPlayers();
    var goldNode = nodes[order[0].id];
    var margin = order.length > 1
      ? Math.max(0, Math.min(1, (order[0].score - order[1].score) / order[0].score))
      : 1;
    var period = LEADERBOARD_GOLD_SWEEP_FAST_MS + margin * (LEADERBOARD_GOLD_SWEEP_SLOW_MS - LEADERBOARD_GOLD_SWEEP_FAST_MS);
    var phase = (now % period) / period;
    var pos = smoothstep(phase) * 160 - 30;
    goldNode.sweep.style.transform = "translateX(" + pos.toFixed(1) + "%) skewX(-18deg)";
    var hump = Math.sin(phase * Math.PI);
    goldNode.li.style.boxShadow =
      "0 0 " + (6 + hump * 22).toFixed(1) + "px rgba(" + LEADERBOARD_GOLD_RGB + "," + (0.25 + hump * 0.45).toFixed(3) + "), " +
      "0 " + (2 + hump * 9).toFixed(1) + "px " + (10 + hump * 16).toFixed(1) + "px rgba(0,0,0," + (0.22 * hump).toFixed(3) + ")";
    goldNode.li.style.background = "rgba(" + LEADERBOARD_GOLD_RGB + "," + (0.12 + hump * 0.26).toFixed(3) + ")";
    requestAnimationFrame(sweepFrame);
  }

測定値: 面積変化12.9%、強度36.3、動いたフレームは23/23です。

06指を離しても慣性で滑り続けるランキング

指を離すと、そのときの速度が coastStep() に渡り、毎フレーム LEADERBOARD_SCROLL_FRICTION を掛けながらオフセットに加算し続け、LEADERBOARD_SCROLL_MIN_VELOCITY を下回るか端に達した時点で自分でループを止めます。摩擦の値を小さくすると同じ勢いでもすぐ止まり、大きくするとずっと長く滑り続けます。role="list"・tabindex="0" のビューポートは、矢印キーを固定値の LEADERBOARD_SCROLL_STEP_PX、Page Up/Down をビューポート自身の高さぶんだけ動かす onKeydown() も別に備えています。

指を離しても慣性で滑り続ける順位リスト
新しいタブで開く

06 · 指を離しても慣性で滑り続ける順位リスト — 計測 累積面積 15.895% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LEADERBOARD_SCROLL_FRICTIONcoastStepLEADERBOARD_SCROLL_MIN_VELOCITY
  function coastStep() {
    if (!coasting) return;
    offset += velocity;
    velocity *= LEADERBOARD_SCROLL_FRICTION;
    if (offset < 0) { offset = 0; velocity = 0; }
    if (offset > maxScroll) { offset = maxScroll; velocity = 0; }
    render();
    if (Math.abs(velocity) < LEADERBOARD_SCROLL_MIN_VELOCITY || (offset === 0 || offset === maxScroll)) {
      coasting = false;
      announceTop();
      return;
    }
    requestAnimationFrame(coastStep);
  }

測定値: 面積変化15.9%、強度79.1、動いたフレームは10/23です。

07スコア変化量に合わせて点滅する矢印バッジ

flashArrow() はスコア差の符号で矢印の向きを、LEADERBOARD_DELTA_MAX_REF に対する大きさで矢印の拡大率を決めたあと、Math.exp(-t / LEADERBOARD_DELTA_DECAY_TAU_MS) で不透明度を毎フレーム減衰させ、LEADERBOARD_DELTA_FLOOR を下回った時点でループ自身を止めます。同じ減衰値が別のアニメーションとしてではなく、行の背景グラデーションと box-shadow にもそのまま使われるため、大きなスコア変化ほど明るく長く光ります。

増減を知らせる矢印バッジ
新しいタブで開く

07 · 増減を知らせる矢印バッジ — 計測 累積面積 31.854% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

flashArrowLEADERBOARD_DELTA_DECAY_TAU_MSLEADERBOARD_DELTA_MAX_REF
    var start = null;
    el.style.opacity = "1";
    function frame(now) {
      if (start === null) start = now;
      var t = now - start;
      var alpha = Math.exp(-t / LEADERBOARD_DELTA_DECAY_TAU_MS);
      if (alpha < LEADERBOARD_DELTA_FLOOR) {
        el.style.opacity = "0";
        row.li.style.background = "";
        row.li.style.boxShadow = "";
        return;
      }
      el.style.opacity = alpha.toFixed(3);
      var a = alpha * mag;
      row.li.style.background = "linear-gradient(90deg, rgba(" + rgb + "," + (a * 0.55).toFixed(3) + "), rgba(" + rgb + ",0) 78%), " + INK_HEX;
      row.li.style.boxShadow = "0 " + (2 + a * 6).toFixed(1) + "px " + (10 + a * 16).toFixed(1) + "px -4px rgba(" + rgb + "," + (a * 0.5).toFixed(3) + ")";
      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);

測定値: 面積変化31.9%、強度27.7、動いたフレームは23/23です。

08スコアの比率だけ伸びる表彰台のバー

applyBars() は各スコアを1位のスコアで割った比率を0.06で下限クランプし、固定高さの枠(LEADERBOARD_PODIUM_BAR_MAX_PX)の中で scaleY() にそのまま使うので、伸び縮みがレイアウトに一切影響しません。同じ比率が BAR_LO_RGB から BAR_HI_RGB への色の補間と足元の影にも使われ、slotOrder という別の固定配列が、実際の順位が入れ替わっても1位を常に中央の列に置き続けます。

スコア分だけ伸びるポディウムバー
新しいタブで開く

08 · スコア分だけ伸びるポディウムバー — 計測 累積面積 18.14% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyBarsLEADERBOARD_PODIUM_BAR_MAX_PXslotOrder
  function applyBars(order) {
    var maxScore = order[0].score || 1;
    order.forEach(function (p, i) {
      var ratio = Math.max(0.06, p.score / maxScore);
      var node = nodes[p.id];
      node.bar.style.transform = "scaleY(" + ratio.toFixed(3) + ")";
      node.rank.textContent = String(i + 1);
      node.score.textContent = String(p.score);
      var r = Math.round(BAR_LO_RGB[0] + (BAR_HI_RGB[0] - BAR_LO_RGB[0]) * ratio);
      var g = Math.round(BAR_LO_RGB[1] + (BAR_HI_RGB[1] - BAR_LO_RGB[1]) * ratio);
      var b = Math.round(BAR_LO_RGB[2] + (BAR_HI_RGB[2] - BAR_LO_RGB[2]) * ratio);
      node.bar.style.background = "rgb(" + r + "," + g + "," + b + ")";
      node.wrap.style.boxShadow = "0 " + (ratio * 9).toFixed(1) + "px " + (ratio * 20).toFixed(1) + "px rgba(10,10,14," + (ratio * 0.34).toFixed(3) + ")";
      node.col.style.background = "rgba(" + BAR_HI_RGB.join(",") + "," + (Math.max(0, ratio - 0.45) * 0.16).toFixed(3) + ")";
    });
  }

測定値: 面積変化18.1%、強度167.7、動いたフレームは23/23です。

09同点の行が同じ色で揃って点滅する

computeRanks() は標準的な同着順位の規則を適用し、直前の行と同じスコアなら同じ順位を引き継いで数えを進めないため、2位や3位が複数行で共有され、次に違うスコアの行はその分だけ順位が飛びます。relayout() は tieRanks で同じ順位の行数を数え、2行以上ある組にだけ lbt__row--tie クラスを付けるので、同点の行は1本に重なるのではなく、同じ黄色い lbt-tie-flash キーフレームで揃って点滅します。

同点を一行にまとめる点滅
新しいタブで開く

09 · 同点を一行にまとめる点滅 — 計測 累積面積 17.028% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

computeRanksLEADERBOARD_TIE_STEPtieRanks
  function relayout(announce) {
    var order = sortedPlayers();
    var ranks = computeRanks(order);
    var tieRanks = {};
    ranks.forEach(function (r, i) {
      tieRanks[r] = (tieRanks[r] || 0) + 1;
    });
    order.forEach(function (p, i) {
      list.appendChild(nodes[p.id].li);
      nodes[p.id].rank.textContent = String(ranks[i]);
      nodes[p.id].score.textContent = String(p.score);
      var isTie = tieRanks[ranks[i]] > 1;
      nodes[p.id].li.classList.toggle("lbt__row--tie", isTie && !reduce);
      nodes[p.id].li.classList.toggle("lbt__row--tie-static", isTie && reduce);
    });
  }

測定値: 面積変化17.0%、強度61.6、動いたフレームは9/23です。

3つのバリエーション — 弾み・桁ロール・慣性を振り切る

項目 定数 標準 → 振り切り 感触
02 LEADERBOARD_RANKUP_OVERSHOOT_PERCENT 15 → 45 値を小さくすると順位が上がっても弾みが小さく落ち着き、値を大きくするとスプリングが大きく飛び出してから戻る、誇張されたポップになる
03 LEADERBOARD_DIGIT_ROLL_STAGGER_MS 30 → 120 値を短くすると桁がほぼ同時に変わり、値を長くすると一桁ずつはっきり回転していくのが見える
06 LEADERBOARD_SCROLL_FRICTION 0.92 → 0.98 値を小さくすると指を離した後すぐに止まり、値を大きくすると摩擦が弱まりずっと長く滑り続ける

どこで壊れるか — 連続入力・同点・ライブラリなしの限界

LEADERBOARD_FLIP_DURATION_MS の再生が終わる前に同じ行がもう一度動かされると、flipSwap() は node.anim.cancel() で前のアニメーションを打ち切り、その時点の位置から新しく再生し直します。これは事故ではなく意図した設計で、02番が popRow() を毎回新しい requestAnimationFrame() ループとして呼び直すのと同じ考え方です。ただし、1秒間に何度もスコアが届くデータソースにつなぐと、行が落ち着く暇なくずっとアニメーション中になり続ける点は先に知っておく価値があります。

順位の数字を表示する01番・02番・04番・05番・08番は、09番のように同点に共通の順位を割り当てる仕組みを持っていません — これらの sortedPlayers() はどれもスコアの降順で並べる単純な Array.prototype.sort() で、本当に同点が起きた場合はどちらが3位でどちらが4位かを選手データの元の並び順だけが決めます(順位の数字自体を表示しない03番・06番・07番にはこの曖昧さは現れません)。同点が頻繁に起こる実データを扱うなら、09番の順位計算をこの5つのどれかに足してから使うのが安全です。ここにある9パターンはどれも、並び替え・整列・アニメーションのためのライブラリを一切呼び出していません — Array.prototype.sort()・getBoundingClientRect()・requestAnimationFrame() だけで動いており、いずれも MDN の requestAnimationFrame() のページに説明があります。この違いを自分の目で確かめられる、9パターン全部の index.html・style.scss を開くための udavckhu は、まさにこの段落の最後、ライブラリなしの限界を読み終えた読者のために置いています。

9パターンを面積と強度で並べ直す

9つを横に並べてみると、上で説明した計算の重さが数字としてはっきり見えてきます。

項目 面積変化 強度 動いたフレーム
01 FLIP順位交換 33.7% 29.9 7/23
02 順位上昇スプリング 23.8% 160.4 22/23
03 桁ロールカウンター 41.5% 68.6 8/23
04 影の浮き上がり 21.3% 60.6 11/23
05 金色の光沢 12.9% 36.3 23/23
06 慣性スクロール 15.9% 79.1 10/23
07 点滅矢印 31.9% 27.7 23/23
08 表彰台のバー 18.1% 167.7 23/23
09 同点点滅 17.0% 61.6 9/23

面積が最も大きく変わるのは03番の桁ロール(41.5%)で、3本のリールが一度に描き直されるためです。一方、フレームあたりの強度が最も高いのは08番の表彰台(167.7)で、毎フレーム高さ・色・影を同じ要素にまとめて書き込んでいるからにほかなりません。05番・07番・08番は23フレームすべてで動きが検出されており、順位が変わった瞬間だけでなく常時ループが回り続けていることを示しています。反対に01番は7/23と最も動きが少なく、実際にスコアが入れ替わった瞬間しか反応しません。

アクセシビリティと reduced-motion — 9パターンそれぞれの止まり方

prefers-reduced-motion: reduce への応答はひとつの共通スイッチではなく、項目ごとに違います。01番の flipSwap() は並び替え直後に即座に処理を終えるためFLIPアニメーション自体が始まらず、02番の popRow() も同様に弾みだけを省略し、順位のテキストは relayout() で即座に更新されます。03番は桁リールの transitionDelay と transitionDuration をどちらも 0ms にして3桁が同時に止まるようにし、04番は x を requestAnimationFrame() で補間する代わりに目標の影を直接描きます。05番の光沢ループはそもそも起動せず、07番の矢印は指数減衰を実行する代わりに不透明度と背景を即座に消し、09番は点滅する lbt-tie-flash キーフレームの代わりに、動かないまま同点だとわかる輪郭線のクラスに切り替わります。

06番は最初からキーボード操作を前提に作られている項目で、tabindex="0"・ビューポートの role="list"・onKeydown() の矢印キー/Page Up・Down は reduced-motion でもそのまま動き続けます — 動作が減るのは指を離した後の慣性部分だけです。08番の自動更新ループは同じメディアクエリの内側にあり reduced-motion では動きませんが、手動でスコアを更新した場合はバーの高さがスケールのトゥイーンなしにそのまま描き直されます。クエリ自体の仕様は MDN の prefers-reduced-motion のページにまとまっています。

汎用の配布素材と、この9パターンは何が違うのか?

素材集にあるランキング デザイン フリー素材の多くは見た目のレイアウトだけを配布していて、順位が変わった瞬間の動きまでは含んでいません。ここにある9パターンはその逆で、見た目そのものより「変化の前後をどう補間するか」が本体です。01番と04番はまったく同じ並び替えイベントに反応しますが、一方は行そのものを動かし、もう一方は影しか動かさない、という違いが起きるのもそのためです。

順位表 デザインとして比較する際に見分けやすいのは、並び替えのたびに行が同じ data-id を保ち続けているかどうかです。ここでは flipSwap() も elevationForX() の呼び出し元も、この一点を頼りに「並び替え前後で同じ行がどれか」を判定しています。

関連する2本もあわせて読んでみてください。09番の同点処理は、よく似た並び替えイベントから挿入位置を計算するドラッグアンドドロップ 並び替えリスト — 挿入先インデックス計算を重ねた9種と相性がよく、08番の比例バーは同じ比率計算を別の起点から使うcss プログレスバー 9選 — ゲーム風に満ちる9つの方式に近い発想です。順位の数字そのものを飾りたい場合はcss リスト デザイン 9選 — マーカーが計算で動く箇条書きの装飾も参考になります。

この9パターンを組み込む前によくある質問

この9つはどれかのUIライブラリやアニメーションライブラリに依存していますか?

いいえ。弾みの比率・減衰の速さ・スクロールの摩擦のように、一般的な既製の配布素材ならライブラリの内部に隠れているような値が、ここではファイルの先頭にある1行の変数として置かれています。使っているブラウザ機能も Array.prototype.sort()・getBoundingClientRect()・Web Animations API・requestAnimationFrame() だけです。

9つのうちどれが一番描画の負荷が高いですか?

強度で見ると08番の表彰台(167.7)と02番のスプリング(160.4)が高く、どちらも同じ要素の複数のスタイルを毎フレーム書き換えています。面積では03番の桁ロール(41.5%)が最も広く塗り替えますが、それは実際に桁が回っている短い間だけで、それ以外は他の項目と同じく静止しています。

9つのうち2つを同じランキングで組み合わせられますか?

はい。04番の影の浮き上がりと01番のFLIP交換は、同じ並び替えイベントの上で衝突なく動くように書かれています — 一方は transform だけ、もう一方は box-shadow だけしか触らないためです。07番の矢印も自分自身の delta だけを読んで動くので、他の項目のどれとでも比較的安全に組み合わせられます。

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

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