Skip to content

Leaderboard

MD · 1950B gzip budget

Ranked list with position, score, and avatar display.

Category
Data display
Budget
md, several states or a live subscription
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Leaderboard

  1. Aisha100

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/leaderboard.json

Copies the source into your project. Pulls in 1 shared registry item: cn.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

import { cn } from "../lib/cn";

export interface LeaderboardEntry {
  id: string;
  name: string;
  avatar?: string;
  score: number;
}

export interface LeaderboardProps {
  entries: LeaderboardEntry[];
  title?: string;
  className?: string;
}

const POSITION_STYLES: Record<number, string> = {
  1: "bg-yellow-400 text-yellow-900",
  2: "bg-neutral-300 text-neutral-700 dark:bg-neutral-600 dark:text-neutral-200",
  3: "bg-orange-400 text-orange-900",
};

/**
 * Ranked leaderboard with avatars and scores. Each row uses `aria-label`
 * with the full rank, name, and score. The list is an `<ol>` for correct
 * semantics.
 */
export function Leaderboard({ entries, title = "Leaderboard", className }: LeaderboardProps) {
  const sorted = [...entries].sort((a, b) => b.score - a.score);

  return (
    <div
      role="region"
      aria-label={title}
      className={cn("flex flex-col gap-3", className)}
    >
      <h2 className="text-lg font-bold text-neutral-900 dark:text-neutral-100">{title}</h2>
      <ol className="flex flex-col gap-1" aria-label="Rankings">
        {sorted.map((entry, index) => {
          const position = index + 1;
          return (
            <li
              key={entry.id}
              aria-label={`Rank ${position}: ${entry.name}, ${entry.score} points`}
              className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white px-4 py-3 dark:border-neutral-800 dark:bg-neutral-950"
            >
              <span
                aria-hidden="true"
                className={cn(
                  "flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-bold",
                  POSITION_STYLES[position] ?? "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400",
                )}
              >
                {position}
              </span>
              {entry.avatar ? (
                <img
                  src={entry.avatar}
                  alt=""
                  aria-hidden="true"
                  className="size-8 shrink-0 rounded-full"
                />
              ) : (
                <span
                  aria-hidden="true"
                  className="flex size-8 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-xs font-medium text-neutral-600 dark:bg-neutral-700 dark:text-neutral-300"
                >
                  {entry.name.charAt(0).toUpperCase()}
                </span>
              )}
              <span className="flex-1 text-sm font-medium text-neutral-900 dark:text-neutral-100">
                {entry.name}
              </span>
              <span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
                {entry.score.toLocaleString()}
              </span>
            </li>
          );
        })}
      </ol>
    </div>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation