← All posts
tutorialleaderboardesportsrivedata

How to build an animated leaderboard for a live stream

Build a leaderboard overlay that updates itself: merge your roster with a live stats feed into one animated, always-correct standings graphic.

TQ

Team Quadraviz

July 16, 2026 · 7 min read

How to build an animated leaderboard for a live stream

A leaderboard is two different pieces of information pretending to be one graphic. There's the roster — names, photos, teams, the stuff that barely changes — and there's the live standings — points, kills, placement, the stuff that changes every few seconds. Most DIY leaderboards are built by hand-editing both into the same template, which is why they're almost always either out of date or missing a photo.

This guide builds a leaderboard the right way: your roster lives in one place, your live stats feed in from another, and Quadraviz merges them into one row-per-player standings graphic that updates itself. Design once, and it stays correct for the rest of the season.

Why leaderboards break in practice

Two failure modes cover most of what goes wrong on stream:

  • The manual leaderboard. Someone retypes standings into a spreadsheet or graphics tool between rounds. It's accurate for about ten minutes after each update, then drifts. Add a fast-moving match and the leaderboard is visibly behind reality.
  • The stats-only leaderboard. A live feed gives you player IDs and scores, but no photos or display names — because that data was never in the feed to begin with. You end up with a leaderboard that shows p_884271: 42 pts instead of a player card.

The fix isn't picking one source over the other. It's combining them.

The model: roster + live stats = one leaderboard

Think of it as two lists that share a key:

Your roster (set up once, rarely changes):

json
[
  { "playerId": "884271", "handle": "Soren", "team": "Wraiths", "photoUrl": ".../soren.png" },
  { "playerId": "884299", "handle": "Nova", "team": "Hex", "photoUrl": ".../nova.png" }
]

Your live stats feed (changes constantly, during the match):

json
[
  { "playerId": "884271", "points": 42, "kills": 11 },
  { "playerId": "884299", "points": 38, "kills": 9 }
]

Neither list alone makes a leaderboard. Joined on playerId, you get exactly what you need per row: name, photo, team, and current points — always in sync, because the join happens live, every time the stats feed updates.

This is the same idea as a SQL join, except there's no code to write — you point Quadraviz at both sources and tell it which field they share.

Step 1 — Set up your roster as a resource

In the Quadraviz dashboard, define a category for your roster — call it "Players" — with the fields you need: handle, team, photo, and whatever else the card design calls for. Add your records once, at the start of the season or tournament. This is your data library: reusable, and not something you touch again until the roster changes.

Step 2 — Connect the live stats feed

Point Quadraviz at your scoring system's endpoint the same way you would for any live data source — an API you poll, or a webhook the source pushes to you. If you don't have a stats API and someone is tracking points by hand, a Google Sheet works too; anything that can be polled or pushed can feed the merge.

ts
const channel = quadraviz.channels.update(channelId, {
  dataSource: {
    type: 'api',
    url: 'https://api.tournament.gg/standings/123',
    pollMs: 3000,
    map: {
      playerId: 'id',
      points: 'score',
      kills: 'stats.kills',
    },
  },
});

Prefer no code? Do the same mapping in the dashboard's data panel — the snippet above and the visual wiring board produce the same result.

Step 3 — Merge the two sources

This is the step that makes it a leaderboard instead of two disconnected lists. In the wiring board, add a combine step: pick your Players resource as one input, your live stats feed as the other, and set playerId as the shared key on both sides.

The output is a single merged list — one row per player, roster fields and live stats fields together — sorted by whatever field you choose. Sort by points descending and you have live standings, ranked, updating as the feed updates.

Step 4 — Design the leaderboard scene in Rive

Build a single row template in Rive — photo, handle, team badge, points — and a "list" behavior that repeats it. Keep the row itself simple; the leaderboard's job is legibility at a glance, not an art piece per row.

A few things worth getting right:

  • Fixed row height. Rank changes should re-order rows, not resize them — jumpy row heights make position changes hard to read.
  • A rank number or medal state for the top three. Viewers scan for "who's winning" before anything else.
  • A transition when rank changes. A row sliding up or down as standings shift reads as "live" in a way a static re-sort never does. Keep it under 400 ms so a flurry of updates doesn't turn into visual noise.

Expose the row's bindable fields — handle, teamName, photo, points — the same way you would for any Rive scene, then upload the .riv file as a scene in Quadraviz.

Step 5 — Bind the merged list and go live

Back in the dashboard, bind your merged leaderboard list to the scene's row list. Quadraviz repeats the row template once per entry and keeps it sorted as the underlying data changes — you don't re-bind anything when a player's score updates, only when the season's roster changes.

Stage the scene in preview, confirm the rows look right with real data, then push the channel live. From here, the leaderboard maintains itself: the stats feed changes, the merge re-runs, the rows re-sort, and the graphic reflects it — no one touching a dashboard mid-round.

Common problems and fixes

A player shows up with no name or photo. Their playerId in the stats feed doesn't match a record in your roster. Usually a typo or a mismatched ID format (numeric vs. string) between the two sources — check that the join key is identical on both sides.

The leaderboard re-sorts too aggressively. If your feed updates every second and every update triggers a full re-sort animation, the board never settles. Either lower the poll interval or add a short debounce so rows settle before the next re-sort.

Rows overlap or clip on a long roster. Cap the visible rows (top 10, top 5) rather than rendering the full roster — a leaderboard with 40 rows isn't readable at broadcast size regardless of how it's built.

A player is missing from the leaderboard entirely. They exist in the roster but not yet in the stats feed — common at the very start of a match, before anyone has scored. Give new entries a default points: 0 in your mapping so they render at the bottom instead of not rendering at all.

Doing this with Quadraviz

The merge-and-render pattern above is the core of what Quadraviz is for — most of what makes a leaderboard hard to maintain elsewhere (keeping roster and live stats in sync, re-sorting on update, animating rank changes) is handled by the platform once you've wired it up.

  • No code required for the merge, the sort, or the row repeat — all of it is drag-and-drop wiring.
  • Free tier with one channel, enough to build and run this exact leaderboard end to end.
  • Rive-native animation for the rank-change transitions, without hand-writing CSS or JS.

Start a free account and you can have a live, self-updating leaderboard running in under half an hour.