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
- command-palette4,210 installs
- stat-card3,982 installs
- nav-tree2,847 installs
- copy-button980 installs
Rank, name and value. Figures line up in a column.
With detail and your row
- Mara Okafor42 components shipped12,480 points
- Jun Takeda31 components shipped9,305 points
- You27 components shipped8,911 points
- Lena Voss19 components shipped6,042 points
- 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
| Prop | Type | Default | Description |
|---|---|---|---|
| entries | { name: string; value: string; detail?: string }[] | Rows in rank order, best first. | |
| unit | string | Read after each value by screen readers. | |
| highlight | number | One-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