Skip to content
goodtabs UI
BrowseBrowse components

Leaderboard

Ranked rows with a rank, a name and a value in tabular figures.

npx shadcn@latest add https://ui.goodtabs.co/r/leaderboard.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/leaderboard

  1. command-palette4,210 installs
  2. stat-card3,982 installs
  3. nav-tree2,847 installs
  4. copy-button980 installs

Rank, name and value. Figures line up in a column.

With detail and your row

  1. Mara Okafor42 components shipped12,480 points
  2. Jun Takeda31 components shipped9,305 points
  3. You27 components shipped8,911 points
  4. Lena Voss19 components shipped6,042 points
  5. Diego Ramos9 components shipped1,208 points

A second line per entry, and the viewer's own rank filled.

Usage

import { Leaderboard } from "@/components/ui/leaderboard"

<Leaderboard entries={entries} unit="points" highlight={3} />

Props

PropTypeDefaultDescription
entries{ name: string; value: string; detail?: string }[]Rows in rank order, best first.
unitstringRead after each value by screen readers.
highlightnumberOne-based rank to fill, for the viewer's own row.

Source

leaderboard/leaderboard.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

export type LeaderboardEntry = {
  name: string;
  /** Already formatted, for example "12,480". */
  value: string;
  /** Small text under the name. */
  detail?: string;
};

type LeaderboardProps = Omit<React.ComponentProps<"ol">, "children"> & {
  entries: LeaderboardEntry[];
  /** Unit read after each value by screen readers, for example "points". */
  unit?: string;
  /** Rank the viewer holds. That row gets a fill. */
  highlight?: number;
};

/**
 * Ranked rows: rank, name, value. Tabular figures keep the columns aligned.
 * The list is ordered, so the rank is also the list position for screen readers.
 */
export function Leaderboard({ entries, unit, highlight, className, ...props }: LeaderboardProps) {
  return (
    <ol
      data-slot="leaderboard"
      className={cn("m-0 flex w-full list-none flex-col gap-px p-0 text-sm", className)}
      {...props}
    >
      {entries.map((entry, i) => (
        <li
          key={i}
          aria-current={highlight === i + 1 ? "true" : undefined}
          className={cn("flex min-h-10 items-center gap-3 rounded-md px-2 py-1", highlight === i + 1 && "bg-muted")}
        >
          <span aria-hidden="true" className="w-5 text-right font-mono text-xs text-muted-foreground tabular-nums">
            {i + 1}
          </span>
          <span className="flex min-w-0 flex-1 flex-col">
            <span className="truncate">{entry.name}</span>
            {entry.detail && <span className="truncate text-xs text-muted-foreground">{entry.detail}</span>}
          </span>
          <span className="font-medium tabular-nums">
            {entry.value}
            {unit && <span className="sr-only"> {unit}</span>}
          </span>
        </li>
      ))}
    </ol>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/leaderboard/prompt.md, or the Copy AI prompt button above.

When to use

  • Top lists by installs, points or views
  • Short rankings next to a stat or a chart

When not to use

  • Long sortable data: use a table
  • Trends over time: use a chart

Agent notes

  • Entries must already be sorted; rank is the position
  • It renders an ordered list, so the rank number is hidden from screen readers and the list order carries it
  • Format the value before passing it
  • Server component: no state, no handlers