GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Leaderboard UI Design: 9 Rank-Change List Patterns

This leaderboard ui design is built on one feel: A leaderboard shifts like runners trading places at the line.

Auto-plays · click a tile to jump to its section · all nine in one zip

Every item below ties one numeric constant to one calculation that decides how a rank change actually plays out on screen.

Nothing here reaches for a charting or animation package. Each item keeps its motion behind a single named constant — LEADERBOARD_FLIP_DURATION_MS, LEADERBOARD_RANKUP_OVERSHOOT_PERCENT, LEADERBOARD_SCROLL_FRICTION, and six more like them — so the number you'd want to tune during a real leaderboard animation css pass is never buried inside a formula three files away from where it's used.

01Rows That FLIP Into Their New Rank Instead of Jumping

When two scores swap places, flipSwap() re-appends every <li> in score order and compares each row's rect from before the reorder against its rect after, then animateFlipRow() inverts that pixel distance into a starting translateY() offset and plays it back to zero over LEADERBOARD_FLIP_DURATION_MS — the classic First-Last-Invert-Play sequence, run through the Web Animations API rather than a CSS transition. Box-shadow and background tint ride the exact same keyframes as the position, and if the same row gets bumped again mid-flight, node.anim.cancel() throws away the old animation and starts fresh from wherever the row currently sits.

Leaderboard Rows That FLIP-Swap Rank Positions
Open in a new tab

01 · Leaderboard Rows That FLIP-Swap Rank Positions — Measured area 33.704% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

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 = "";
    };
  }

Measured: 33.7% of the frame area changes across a swap, intensity averages 29.9, and 7 of 23 sampled frames moved.

02A Row That Overshoots Its Target When It Ranks Up

springScale() back-solves a damping ratio zeta from LEADERBOARD_RANKUP_OVERSHOOT_PERCENT using the closed-form second-order step-response formula against a fixed natural frequency, then popRow() evaluates that spring curve itself inside its own requestAnimationFrame() loop rather than stepping a physics simulation tick by tick. A higher overshoot percent pushes the peak scale further past 1 before the row settles back down — that peak is exactly what the second variant below turns up.

Row That Springs Up When Its Rank Improves
Open in a new tab

02 · Row That Springs Up When Its Rank Improves — Measured area 23.776% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

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));
  }

Measured: 23.8% of the frame area changes, intensity averages 160.4, and 22 of 23 sampled frames moved.

03A Score Counter That Rolls Up One Digit at a Time

rollTo() splits the target score into three digits and staggers each digit reel's transitionDelay by its place index times LEADERBOARD_DIGIT_ROLL_STAGGER_MS, so the hundreds place lands, then the tens place, then the ones place, instead of all three snapping together. Each reel itself is a translateY() of digit × LEADERBOARD_DIGIT_H, and only the digit that actually changed value gets an is-rolling highlight class — a jump from 780 to 781 lights up one cell, a jump to 891 lights up all three.

Score Counter That Rolls Up Digit by Digit
Open in a new tab

03 · Score Counter That Rolls Up Digit by Digit — Measured area 41.486% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

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;
    }

Measured: 41.5% of the frame area changes, intensity averages 68.6, and 8 of 23 sampled frames moved.

04A Row Shadow That Lifts Higher the Better the Rank

elevationForX() takes one normalized value x between 0 and 1 (a row's rank, where being closer to the top means closer to 1) and turns it into every shadow property at once — blur and offset each start from a small base and scale up to LEADERBOARD_ELEVATION_BLUR_MAX_PX and LEADERBOARD_ELEVATION_OFFSET_MAX_PX, alpha rides LEADERBOARD_ELEVATION_ALPHA_MAX, and the card's own lift off the row beneath it scales with LEADERBOARD_ELEVATION_LIFT_MAX_PX. When a re-sort changes a row's rank, tweenRow() eases that one x value through requestAnimationFrame() at a duration it reads straight from the --lbe-duration CSS custom property, instead of letting a CSS transition jump the shadow the instant a reorder happens mid-frame.

Row Whose Shadow Lifts Higher With Rank
Open in a new tab

04 · Row Whose Shadow Lifts Higher With Rank — Measured area 21.265% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

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));
  }

Measured: 21.3% of the frame area changes, intensity averages 60.6, and 11 of 23 sampled frames moved.

05A Gold Sweep That Only Plays Across the No. 1 Row

sweepFrame() re-measures the score gap between rank 1 and rank 2 on every tick as a ratio between 0 and 1, then interpolates that ratio straight into a sweep period between LEADERBOARD_GOLD_SWEEP_FAST_MS and LEADERBOARD_GOLD_SWEEP_SLOW_MS — a tight race sweeps the light band fast, a comfortable lead sweeps it slow, with nothing so coarse as a single close/far cutoff. smoothstep() eases the band's own position across the row while a synced Math.sin() hump brightens the row's background and box-shadow only near the middle of each pass.

Gold Gloss That Sweeps Only Across the No. 1 Row
Open in a new tab

05 · Gold Gloss That Sweeps Only Across the No. 1 Row — Measured area 12.883% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

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);
  }

Measured: 12.9% of the frame area changes, intensity averages 36.3, and 23 of 23 sampled frames moved.

06A Ranking List That Keeps Gliding After You Let Go

Releasing a drag hands the pointer's exit velocity to startCoast(), and coastStep() adds that velocity to the scroll offset every frame while multiplying it by LEADERBOARD_SCROLL_FRICTION, stopping itself once the value drops below LEADERBOARD_SCROLL_MIN_VELOCITY or the list hits either end — the same flick stops almost immediately at a low friction value and coasts much further at a high one. The viewport (role="list", tabindex="0") also answers Arrow Up/Down with a fixed LEADERBOARD_SCROLL_STEP_PX and Page Up/Down with its own clientHeight, entirely independent of the coasting physics.

Ranking List That Keeps Gliding After You Let Go
Open in a new tab

06 · Ranking List That Keeps Gliding After You Let Go — Measured area 15.895% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

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);
  }

Measured: 15.9% of the frame area changes, intensity averages 79.1, and 10 of 23 sampled frames moved.

07An Arrow Badge That Flashes to Match the Score Jump

flashArrow() sets the arrow's scale from the delta's size relative to LEADERBOARD_DELTA_MAX_REF and its direction from the delta's sign, then decays an alpha value with Math.exp(-t / LEADERBOARD_DELTA_DECAY_TAU_MS) on every frame until it drops under LEADERBOARD_DELTA_FLOOR, at which point the loop clears the arrow's opacity and stops calling itself. That same decaying alpha — not a second, separate animation — also paints the row's background gradient and shadow, so a big score jump flashes brighter and lingers longer than a small one.

Arrow Badge That Flashes the Rank Delta
Open in a new tab

07 · Arrow Badge That Flashes the Rank Delta — Measured area 31.854% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

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);

Measured: 31.9% of the frame area changes, intensity averages 27.7, and 23 of 23 sampled frames moved.

08Podium Bars That Grow in Proportion to Score

applyBars() divides each score by the leader's score and clamps that ratio to a floor of 0.06 before using it directly as the bar's scaleY() inside a fixed-height track (LEADERBOARD_PODIUM_BAR_MAX_PX), so growth never touches layout at all — the same ratio then interpolates the bar's RGB color between BAR_LO_RGB and BAR_HI_RGB, and scales the shadow pooling beneath it the same way. A separate, unrelated constant array (slotOrder) keeps the current leader displayed in the fixed center column even as the score order underneath it changes.

Podium Bar That Grows With the Score
Open in a new tab

08 · Podium Bar That Grows With the Score — Measured area 18.14% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

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) + ")";
    });
  }

Measured: 18.1% of the frame area changes, intensity averages 167.7, and 23 of 23 sampled frames moved.

09Tied Rows That Flash the Same Color Together

computeRanks() applies the standard competition-ranking rule: a score tied with the row directly above it inherits that same rank number instead of counting up, so two or three rows can legitimately share position 2 while the next distinct score jumps straight to 4. relayout() then counts how many rows share each rank in tieRanks and toggles lbt__row--tie on every row in a group of more than one — both tied rows flash together using that same class's yellow lbt-tie-flash keyframe rather than visually overlapping into a single line, and LEADERBOARD_TIE_STEP is the score increment bump() uses to create or break a tie on each tick.

Flash That Merges Tied Rows Into One Line
Open in a new tab

09 · Flash That Merges Tied Rows Into One Line — Measured area 17.028% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

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);
    });
  }

Measured: 17.0% of the frame area changes, intensity averages 61.6, and 9 of 23 sampled frames moved.

Where a Real Game's Leaderboard Actually Earns This Motion

Items 1 and 4 fit any live, real-time game leaderboard ui design where the whole point is watching a challenger overtake the row above it — the FLIP swap in item 1 sells the overtake itself, while item 4's shadow makes rank feel like physical height even before a single number changes. Item 2's overshoot suits a points-and-rewards page where ranking up should feel like a small win worth noticing, and item 3's rolling counter reads naturally on any live score dashboard where the number, not the row order, is the thing a viewer is watching tick over.

Item 5 belongs on a hall-of-fame or all-time-best page where only the very top result deserves extra shine, and item 8's proportional bars suit an awards or results screen where comparing relative score, not just order, is the point. Item 6's inertial scroll is built for a long ranking list on a narrow screen, complete with real arrow-key and Page Up/Page Down support for anyone not using a pointer. Item 7's flashing arrow fits a weekly rank-change report where the size of the jump matters as much as the direction, and item 9's shared-color flash is the one built specifically for quiz or contest results where two players legitimately end up tied.

Nine Rank-Change Patterns, Measured Frame by Frame

Comparing these nine side by side — and against the site's running leaderboard animation css measurements — is where the calculation-layer claim above turns into numbers instead of a promise:

Item Area changed Intensity Frames moved
01 FLIP rank swap 33.7% 29.9 7/23
02 Rank-up spring 23.8% 160.4 22/23
03 Digit-roll counter 41.5% 68.6 8/23
04 Elevation shadow 21.3% 60.6 11/23
05 Gold sweep 12.9% 36.3 23/23
06 Inertial scroll 15.9% 79.1 10/23
07 Flash arrow 31.9% 27.7 23/23
08 Podium bars 18.1% 167.7 23/23
09 Tie flash 17.0% 61.6 9/23

Item 3's digit roll changes the most pixels of any item here (41.5%) since three whole reels repaint at once, but item 8's podium bars run the hottest per changed pixel (intensity 167.7) because every frame rewrites height, color, and shadow together on the same element. Items 5, 7, and 8 move on all 23 sampled frames — each one runs a continuous loop rather than firing once per rank change — while item 1's FLIP swap is the calmest of the nine at 7 of 23, since it only animates the instant an actual swap happens and sits still otherwise.

Three Values Worth Turning Up or Down First

Item Constant Stock → Turned up Feel
02 LEADERBOARD_RANKUP_OVERSHOOT_PERCENT 15 → 45 the rank-up bounce goes from a small nudge to a visible, springy overshoot before it settles
03 LEADERBOARD_DIGIT_ROLL_STAGGER_MS 30 → 120 the three digit reels land far enough apart that you can watch each place roll in on its own beat
06 LEADERBOARD_SCROLL_FRICTION 0.92 → 0.98 a single flick keeps coasting for several extra seconds instead of settling almost immediately

Where These Nine Rank-Change Patterns Break Down

If you trigger a bump on the same row again before its LEADERBOARD_FLIP_DURATION_MS animation has finished, flipSwap() calls .cancel() on the row's running animation and starts a fresh one from wherever it currently sits — that's a deliberate design choice, not an oversight, and it's the same cancel-and-restart approach items 2 and 4 lean on when a second update lands mid-tween. It does mean a data feed that fires several updates a second can leave every row perpetually mid-animation rather than settled, which is worth watching for before wiring one of these into a real polling loop.

Items 1 through 8 also don't compute a shared rank for two equal scores the way item 9 does — sortedPlayers() in each of those eight files is a plain descending Array.prototype.sort() on score, so a genuine tie between, say, position 3 and position 4 still prints as two distinct rank numbers, with only the players' original array order deciding which one reads as 3 and which reads as 4. The <span class="pw-inline" id="pw">udavckhu</span> unlocking every index.html/style.scss pair below sits in this paragraph on purpose: it's worth knowing which of the nine needs item 9's tie-aware rank counting added before you drop a real, tie-heavy dataset into one of the first eight instead of the ninth. None of the nine leaderboard ui design ideas here call out to a charting, sorting, or animation package to do it — Array.prototype.sort(), getBoundingClientRect(), and requestAnimationFrame() are the only moving parts, and all three are documented directly on MDN's requestAnimationFrame() page.

Accessibility and prefers-reduced-motion Across These Nine Rank Changes

Each item answers prefers-reduced-motion: reduce on its own terms rather than through one shared switch. Item 1's flipSwap() returns right after reordering the rows, so no FLIP animation ever starts; item 2's popRow() does the same, skipping the spring pop while the rank text still updates instantly through relayout(); item 3 forces its digit reels' transition delay and duration to 0ms so all three land at once instead of staggering in. Item 4 paints the target shadow directly instead of tweening x through requestAnimationFrame(), item 5's sweep loop simply never starts, item 7's arrow clears its opacity and background immediately instead of running the exponential decay, and item 9 swaps the flashing lbt-tie-flash keyframe for a static outline class that marks a tie without ever animating.

Item 6 is the one built with real keyboard support in the first place — tabindex="0", a role="list" on the viewport, and the Arrow/Page key handling in onKeydown() all keep working exactly as they do otherwise, since reduced motion only removes the momentum after a pointer release, not the keyboard path itself. Item 8's automatic score-change loop is gated behind the same media query and doesn't run under reduced motion, though a manual score update still repaints the bars at their new height without a scale tween. Full behavior for the query itself is documented in MDN's prefers-reduced-motion entry.

What Actually Makes a Leaderboard Different From a Plain Ranked List?

A plain ranked list only needs to draw the current order; a leaderboard needs to draw the change between the old order and the new one, which is why every item above keeps its own before/after state rather than just re-rendering from a fresh array. That distinction is also why two rows can legitimately look identical at rest and still animate completely differently the moment a score updates — item 1 and item 4 both react to the exact same re-sort event, but one moves the row and the other only moves its shadow.

If you're comparing these nine against a plain leaderboard ui design template you've seen elsewhere, the tell is usually whether each row keeps a stable player id across a reorder — every pattern here relies on that one JS-side id in flipSwap() and elevationForX()'s callers alike to know which row is which before and after the DOM changes shape.

Two related patterns worth reading next: the tied-score handling above pairs naturally with Drag to Reorder UI: 9 Lists, One Drop Index, where a very similar re-sort event drives a drop-index calculation instead of a rank number, and item 8's proportional bars sit close to CSS Progress Bar: 9 Game-Style Fill Patterns, which works through the same ratio-to-fill math from a different starting point. Item 9's ranking numerals also connect back to CSS List Style: 9 Marker Design Patterns for anyone styling the rank digit itself rather than the row around it.

FAQ: Answers About This Leaderboard UI Design

Do any of these nine rank-change patterns depend on a UI or animation library?

No. Every constant a typical leaderboard ui design template hides inside a bundled library — flip duration, overshoot ratio, decay rate, scroll friction — sits as a single plain variable at the top of its own file here instead, and the only browser APIs involved are Array.prototype.sort(), getBoundingClientRect(), the Web Animations API, and requestAnimationFrame().

Which of the nine is the most expensive to keep running continuously?

By intensity, item 8's podium bars (167.7) and item 2's rank-up spring (160.4) run the hottest, since both rewrite several visual properties on the same element every single frame. By area, item 3's digit roll (41.5%) repaints the most pixels per change, but only for the short burst while the reels are actually rolling — outside a rank change it's idle like the rest.

Can two of these nine be combined on the same leaderboard?

Yes — item 4's elevation shadow and item 1's FLIP swap were written to run on the same re-sort event without conflict, since one only ever touches transform on the row and the other only ever touches box-shadow properties. Item 7's flash arrow is similarly safe to pair with almost any of the other eight, because it reads its own delta value and never touches a property another item is animating.

Enter the archive password

The password is inside this article. You will find it as you read.