WebberUI

Rank Shuffle Board

A data-driven rank shuffle leaderboard — rows swap positions with a FLIP animation when scores update, arrows flash for rank gains and losses, and the top three carry gold, silver, and bronze podium vocabulary.

Loading preview…
npx shadcn@latest add https://webberui.com/r/rank-shuffle-board.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<RankShuffleBoard />

Installation

npx shadcn@latest add https://webberui.com/r/rank-shuffle-board.json

Or, once registries are configured in components.json, install it as @webberui/rank-shuffle-board.

Usage

import { RankShuffleBoard } from "@/components/ui/rank-shuffle-board";

const entries = [
  { id: "amy", name: "Amy", score: 2480 },
  { id: "ben", name: "Ben", score: 2210 },
  { id: "cleo", name: "Cleo", score: 1975 },
];

<RankShuffleBoard entries={entries} unit="pt" />

Sorting happens inside the component — just update the scores in entries and the rows swap positions automatically with a FLIP animation. id must be stable (do not use array indices) for FLIP to match positions correctly before and after.

With avatars:

<RankShuffleBoard
  unit="pts"
  entries={members.map((m) => ({
    id: m.id,
    name: m.name,
    score: m.score,
    avatar: <img src={m.avatarUrl} alt="" className="size-8 rounded-full" />,
  }))}
/>

Props

RankShuffleBoard

PropTypeDefaultDescription
entriesRankShuffleEntry[]Leaderboard entries, sorted internally by score from high to low with a stable sort (ties keep their original order)
unitstringUnit text after the score (pt, pts, and so on)
classNamestringAppended to the outermost <ol> class

RankShuffleEntry

FieldTypeDescription
idstringStable unique identifier; the FLIP animation matches each row's before and after positions by it
namestringDisplay name
scorenumberScore, by which the component sorts internally from high to low
avatarReact.ReactNodeAvatar to the left of the name (optional)

How it works

  • FLIP swapping: rows slide continuously to their new ranks with a Motion layout animation (spring), matching before and after positions by the stable id key instead of fading out and repainting
  • Gain / loss arrows: the component records the previous rank on a ref, and any row whose rank changed briefly flashes an arrow (up ↑ green / down ↓ red) that AnimatePresence fades out after 1.5 seconds; another change within those 1.5 seconds resets the direction and the countdown. The arrow sits in a fixed-width slot, so its appearance and disappearance never push the score around
  • Podium vocabulary: the top three carry a gold (amber) / silver (slate) / bronze (orange) leading edge color band, with the rank badge enlarged and matching the same palette; from fourth place onward the palette is neutral
  • Score spring tween: the score is written straight into the DOM through a MotionValue (with no React re-render), springing to the new value, displayed with thousands separators and rounded to whole numbers
  • Stable sort: the original index acts as the tiebreak, so entries with equal scores keep the order they were passed in and never swap for no reason

Accessibility

  • When the user has "reduce motion" enabled at the system level, FLIP is disabled (rows jump straight to their new positions), the score shows its final value directly, and the arrows play no fade in / fade out (keeping the DOM structure consistent with SSR)
  • The score container carries an aria-label (the final formatted value, including the unit), and the animating span mid-tween is hidden from assistive technology (aria-hidden), so the screen reader only ever announces the final value
  • The board is a semantic ordered <ol> list; the gain/loss arrows and the color bands are decorative and are all marked aria-hidden

On this page