05Gold Gloss That Sweeps Only Across the No. 1 Row
A diagonal band of light periodically sweeps across the top row, giving it a soft, repeating shine.
Best forHall-of-fame page
LEADERBOARD_GOLD_SWEEP_FAST_MSsmoothstepsweepFrame

<main class="stage">
<div class="lbg is-demo" id="lbg">
<ul class="lbg__list" id="lbgList" role="list"></ul>
<p class="lbg__sr" id="lbgSr" aria-live="polite"></p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: sweepFrame() 이 1·2위 점수차(marginRatio)로 광택 주기를 다시 재고
// smoothstep() 이징으로 --sweep 위치를 매 프레임 계산한다 (index.html 참고).
.stage { @include stage($stage-ink); }
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-cream;
.lbg {
width: min(88vw, 320px);
display: flex;
flex-direction: column;
gap: $sp-2;
}
.lbg__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: $sp-2;
}
.lbg__row {
position: relative;
display: flex;
align-items: center;
gap: $sp-3;
padding: $sp-2 $sp-3;
border-radius: $r-sm;
background: $subject-ink;
border: 1px solid rgba($subject-cream, .12);
overflow: hidden;
cursor: pointer;
}
.lbg__row:focus-visible {
outline: 2px solid $subject-blue-light;
outline-offset: 2px;
}
.lbg__row--gold {
background: rgba($stage-yellow, .16);
border-color: rgba($stage-yellow, .55);
}
.lbg__rank {
width: 22px;
flex: 0 0 auto;
font-size: 13px;
font-weight: 700;
color: $subject-blue-light;
text-align: center;
}
.lbg__row--gold .lbg__rank {
color: $stage-yellow;
}
.lbg__name {
flex: 1;
min-width: 0;
font-size: 12px;
font-weight: 700;
color: $color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lbg__score {
flex: 0 0 auto;
font-size: 12px;
font-weight: 700;
color: $color;
font-variant-numeric: tabular-nums;
}
.lbg__sweep {
position: absolute;
top: 0;
left: 0;
width: 55%;
height: 100%;
background: linear-gradient(115deg, transparent, rgba($stage-yellow, .6), transparent);
mix-blend-mode: screen;
pointer-events: none;
will-change: transform;
}
.lbg__row--gold {
will-change: box-shadow, background;
}
.lbg__row:not(.lbg__row--gold) .lbg__sweep {
display: none;
}
.lbg__sr {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip: rect(0 0 0 0); margin: -1px; padding: 0; border: 0;
}
@media (max-width: 340px) {
.lbg { width: min(94vw, 260px); }
.lbg__list { gap: $sp-1; }
.lbg__row { padding: $sp-1 $sp-2; gap: $sp-2; }
.lbg__name { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.lbg__sweep { display: none !important; }
}.stage html, .stage body {
margin: 0;
overflow: hidden;
}
.stage .stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.lbg {
width: min(88vw, 320px);
display: flex;
flex-direction: column;
gap: 8px;
}
.lbg__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.lbg__row {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 8px;
background: #17141a;
border: 1px solid rgba(255, 247, 230, 0.12);
overflow: hidden;
cursor: pointer;
}
.lbg__row:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.lbg__row--gold {
background: rgba(255, 210, 63, 0.16);
border-color: rgba(255, 210, 63, 0.55);
}
.lbg__rank {
width: 22px;
flex: 0 0 auto;
font-size: 13px;
font-weight: 700;
color: #5b8cf8;
text-align: center;
}
.lbg__row--gold .lbg__rank {
color: #ffd23f;
}
.lbg__name {
flex: 1;
min-width: 0;
font-size: 12px;
font-weight: 700;
color: #fff7e6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lbg__score {
flex: 0 0 auto;
font-size: 12px;
font-weight: 700;
color: #fff7e6;
font-variant-numeric: tabular-nums;
}
.lbg__sweep {
position: absolute;
top: 0;
left: 0;
width: 55%;
height: 100%;
background: linear-gradient(115deg, transparent, rgba(255, 210, 63, 0.6), transparent);
mix-blend-mode: screen;
pointer-events: none;
will-change: transform;
}
.lbg__row--gold {
will-change: box-shadow, background;
}
.lbg__row:not(.lbg__row--gold) .lbg__sweep {
display: none;
}
.lbg__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
margin: -1px;
padding: 0;
border: 0;
}
@media (max-width: 340px) {
.lbg {
width: min(94vw, 260px);
}
.lbg__list {
gap: 4px;
}
.lbg__row {
padding: 4px 8px;
gap: 8px;
}
.lbg__name {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.lbg__sweep {
display: none !important;
}
}(function () {
"use strict";
var LEADERBOARD_GOLD_SWEEP_FAST_MS = 520;
var LEADERBOARD_GOLD_SWEEP_SLOW_MS = 1300;
var LEADERBOARD_GOLD_TICK_MS = 460;
var LEADERBOARD_GOLD_RGB = "255,210,63";
function seedRand(seed) {
var s = seed % 2147483647; if (s <= 0) s += 2147483646;
return function () { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; };
}
var rand = seedRand(418);
function smoothstep(x) {
var c = Math.max(0, Math.min(1, x));
return c * c * (3 - 2 * c);
}
var root = document.getElementById("lbg");
var list = document.getElementById("lbgList");
var sr = document.getElementById("lbgSr");
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var players = [
{ id: "p1", name: "AMBER7", score: 940 },
{ id: "p2", name: "FLARE2", score: 905 },
{ id: "p3", name: "QUILL8", score: 870 },
{ id: "p4", name: "NOMAD5", score: 835 },
{ id: "p5", name: "WISP_3", score: 800 }
];
var nodes = {};
function human() {
if (root.classList.contains("is-demo")) root.classList.remove("is-demo");
}
function sortedPlayers() {
return players.slice().sort(function (a, b) { return b.score - a.score; });
}
var prevGoldId = null;
function relayout(announce) {
var order = sortedPlayers();
order.forEach(function (p, i) {
list.appendChild(nodes[p.id].li);
nodes[p.id].rank.textContent = String(i + 1);
nodes[p.id].score.textContent = String(p.score);
nodes[p.id].li.classList.toggle("lbg__row--gold", i === 0);
});
if (order[0].id !== prevGoldId) {
if (prevGoldId && nodes[prevGoldId]) {
nodes[prevGoldId].li.style.boxShadow = "";
nodes[prevGoldId].li.style.background = "";
}
prevGoldId = order[0].id;
}
if (announce) sr.textContent = order[0].name + " is rank 1, score " + order[0].score;
return order;
}
function bump(id) {
var p = players.filter(function (x) { return x.id === id; })[0];
p.score += 15 + Math.floor(rand() * 30);
relayout(true);
}
players.forEach(function (p) {
var li = document.createElement("li");
li.className = "lbg__row";
li.setAttribute("role", "listitem");
li.tabIndex = 0;
var rank = document.createElement("span");
rank.className = "lbg__rank";
var name = document.createElement("span");
name.className = "lbg__name";
name.textContent = p.name;
var score = document.createElement("span");
score.className = "lbg__score";
var sweep = document.createElement("span");
sweep.className = "lbg__sweep";
sweep.setAttribute("aria-hidden", "true");
li.appendChild(rank);
li.appendChild(name);
li.appendChild(score);
li.appendChild(sweep);
nodes[p.id] = { li: li, rank: rank, score: score, sweep: sweep };
li.addEventListener("click", function () { human(); bump(p.id); });
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); human(); bump(p.id); }
});
list.appendChild(li);
});
relayout(false);
// 계산 겹: 1위와 2위의 score 차(marginRatio)로 광택이 흐르는 주기를 매 프레임 다시 잰다 —
// 격차가 클수록 느긋하게, 격차가 좁을수록 빠르게 흐른다. 위치는 smoothstep 로 완만하게 이징한다.
// 같은 위상(phase)을 sin 으로 접어 "쓸고 지나가는 가운데"에서 정점을 찍는 hump 로 만들고,
// 1등 행 배경 톤·그림자(box-shadow, overflow:hidden 밖으로 번짐)에 그대로 전파한다.
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);
}
if (!reduce) {
requestAnimationFrame(sweepFrame);
(function loop() {
if (root.classList.contains("is-demo")) {
var order = sortedPlayers();
var idx = Math.floor(rand() * order.length);
order[idx].score += 12 + Math.floor(rand() * 24);
relayout(true);
}
window.setTimeout(loop, LEADERBOARD_GOLD_TICK_MS);
})();
}
})();import { useEffect, useRef } from "react";
import styles from "./LeaderboardGoldGlossSweep.module.scss";
type Box<T> = { current: T };
const LEADERBOARD_GOLD_SWEEP_FAST_MS = 520;
const LEADERBOARD_GOLD_SWEEP_SLOW_MS = 1300;
const LEADERBOARD_GOLD_TICK_MS = 460;
const LEADERBOARD_GOLD_RGB = "255,210,63";
function seedRand(seed: number) {
let s = seed % 2147483647;
if (s <= 0) s += 2147483646;
return function () {
s = (s * 16807) % 2147483647;
return (s - 1) / 2147483646;
};
}
function smoothstep(x: number) {
const c = Math.max(0, Math.min(1, x));
return c * c * (3 - 2 * c);
}
type Player = { id: string; name: string; score: number };
type RowNodes = { li: HTMLLIElement; rank: HTMLSpanElement; score: HTMLSpanElement; sweep: HTMLSpanElement };
export default function LeaderboardGoldGlossSweep() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const listRef = useRef(null) as unknown as Box<HTMLUListElement>;
const srRef = useRef(null) as unknown as Box<HTMLParagraphElement>;
useEffect(() => {
const root = rootRef.current;
const list = listRef.current;
const sr = srRef.current;
const rand = seedRand(418);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const players: Player[] = [
{ id: "p1", name: "AMBER7", score: 940 },
{ id: "p2", name: "FLARE2", score: 905 },
{ id: "p3", name: "QUILL8", score: 870 },
{ id: "p4", name: "NOMAD5", score: 835 },
{ id: "p5", name: "WISP_3", score: 800 }
];
const nodes: Record<string, RowNodes> = {};
let timer: number | undefined;
let raf: number | undefined;
function human() {
if (root.classList.contains("is-demo")) root.classList.remove("is-demo");
}
function sortedPlayers() {
return players.slice().sort((a, b) => b.score - a.score);
}
let prevGoldId: string | null = null;
function relayout(announce: boolean) {
const order = sortedPlayers();
order.forEach((p, i) => {
list.appendChild(nodes[p.id].li);
nodes[p.id].rank.textContent = String(i + 1);
nodes[p.id].score.textContent = String(p.score);
nodes[p.id].li.classList.toggle(styles["lbg__row--gold"], i === 0);
});
if (order[0].id !== prevGoldId) {
if (prevGoldId && nodes[prevGoldId]) {
nodes[prevGoldId].li.style.boxShadow = "";
nodes[prevGoldId].li.style.background = "";
}
prevGoldId = order[0].id;
}
if (announce) sr.textContent = order[0].name + " 이(가) 1위, 점수 " + order[0].score;
return order;
}
function bump(id: string) {
const p = players.filter((x) => x.id === id)[0];
p.score += 15 + Math.floor(rand() * 30);
relayout(true);
}
players.forEach((p) => {
const li = document.createElement("li");
li.className = styles.lbg__row;
li.setAttribute("role", "listitem");
li.tabIndex = 0;
const rank = document.createElement("span");
rank.className = styles.lbg__rank;
const name = document.createElement("span");
name.className = styles.lbg__name;
name.textContent = p.name;
const score = document.createElement("span");
score.className = styles.lbg__score;
const sweep = document.createElement("span");
sweep.className = styles.lbg__sweep;
sweep.setAttribute("aria-hidden", "true");
li.appendChild(rank);
li.appendChild(name);
li.appendChild(score);
li.appendChild(sweep);
nodes[p.id] = { li, rank, score, sweep };
li.addEventListener("click", () => {
human();
bump(p.id);
});
li.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
human();
bump(p.id);
}
});
list.appendChild(li);
});
relayout(false);
function sweepFrame(now: number) {
const order = sortedPlayers();
const goldNode = nodes[order[0].id];
const margin = order.length > 1
? Math.max(0, Math.min(1, (order[0].score - order[1].score) / order[0].score))
: 1;
const period = LEADERBOARD_GOLD_SWEEP_FAST_MS + margin * (LEADERBOARD_GOLD_SWEEP_SLOW_MS - LEADERBOARD_GOLD_SWEEP_FAST_MS);
const phase = (now % period) / period;
const pos = smoothstep(phase) * 160 - 30;
goldNode.sweep.style.transform = "translateX(" + pos.toFixed(1) + "%) skewX(-18deg)";
const 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) + ")";
raf = requestAnimationFrame(sweepFrame);
}
if (!reduce) {
raf = requestAnimationFrame(sweepFrame);
const loop = () => {
if (root.classList.contains("is-demo")) {
const order = sortedPlayers();
const idx = Math.floor(rand() * order.length);
order[idx].score += 12 + Math.floor(rand() * 24);
relayout(true);
}
timer = window.setTimeout(loop, LEADERBOARD_GOLD_TICK_MS);
};
timer = window.setTimeout(loop, LEADERBOARD_GOLD_TICK_MS);
}
return () => {
if (timer) window.clearTimeout(timer);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.lbg + " is-demo"} ref={rootRef}>
<ul className={styles.lbg__list} ref={listRef} role="list" />
<p className={styles.lbg__sr} ref={srRef} aria-live="polite" />
</div>
</main>
);
}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 · Leaderboard UI Design: 9 Rank-Change List Patterns
The post this demo lives in
Leaderboard UI Design: 9 Rank-Change List PatternsThis leaderboard ui design is built on one feel: A leaderboard shifts like runners trading places at the line. Row 01 gl See all demos in this post
If you like this effect - similar demos
Lists & tables03 Pull-to-Refresh That Sweeps Only the Rows That ChangedInfinite Scroll Implementation — 9 List UI Basics
Lists & tables08 Summary row stuck to the floor that recounts9 Data Table UI Patterns — Drag, Resize, Select
Lists & tables04 Like Count That Rolls the Difference From Its Previous ValueComment Section UI: 9 Thread List Patterns