# Leaderboard

> A React leaderboard ranking people, teams or products by one metric, with medals, bars, rank changes, a podium and a "you" row.

- Type: React chart component (Charts & Stats)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add leaderboard`
- Docs: https://wingo-ui.com/components/leaderboard

## Interaction

Read top to bottom: rank or medal, avatar, name, a bar that shows how far each one is from the leader, the value and the move since last period (a green ▲2, a red ▼1, a gray dash when nothing moved, a blue "New"). The first time it scrolls into view the bars grow and the values count up; when the numbers change, rows glide to their new place and the rank chips pop. With onItemClick every row is a button: Tab lands on the list once, Up / Down, Home / End and PageUp / PageDown move between rows, Enter opens one. "Show all" expands the rest; your own row stays pinned under the top rows with a gap when it falls outside the limit. On a phone rows are at least 44px, the value never truncates (the name does) and the podium keeps three columns down to 320px. Screen readers hear one sentence per row: "Rank 1, Daniel Reed, $48,921, up 1 place". Reduced motion shows the final state at once.

## Installation

```bash
npx wingo-ui@latest add leaderboard
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/leaderboard` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/leaderboard.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Chart kit](https://wingo-ui.com/components/chart-kit) ([Markdown](https://wingo-ui.com/components/chart-kit.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [Trend badge](https://wingo-ui.com/components/trend-badge) ([Markdown](https://wingo-ui.com/components/trend-badge.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

## Usage

```tsx
import { Leaderboard, type LeaderboardItem } from "@/components/ui/leaderboard"

const team: LeaderboardItem[] = [
  { id: "daniel", name: "Daniel Reed", value: 48920.5, avatar: "/demo/mihai-ionescu.jpg", previousRank: 2 },
  { id: "ryan", name: "Ryan Cooper", value: 31200, avatar: "/demo/radu-marin.jpg", previousRank: 1 },
  { id: "emma", name: "Emma Carter", value: 18080, avatar: "/demo/ana-pop.jpg", previousRank: 3 },
  { id: "grace", name: "Grace Kim", value: 14450, avatar: "/demo/maria-toma.jpg", previousRank: 5 },
  { id: "olivia", name: "Olivia Brooks", value: 3450, avatar: "/demo/elena-stan.jpg", previousRank: null },
]

export function TopSellers() {
  return (
    <Leaderboard
      title="Top sellers"
      metric="Revenue, September 2026"
      items={team}
      valueFormat="currency"
      currency="USD"
      locale="en-US"
      highlightId="emma"
      limit={4}
      onItemClick={(item) => console.log(item.id)}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `LeaderboardItem[]` |  | { id, name, value, avatar?, icon?, description?, previousRank?, delta?, href?, searchName? }. Ranked by value unless sort is none; equal values share a rank (1, 1, 3). previousRank null marks a new entry. icon replaces the avatar with a tile (products, channels). |
| `title?` | `ReactNode` | `""` | The heading above the list; also the list's accessible name. |
| `metric?` | `ReactNode` | `""` | The caption under the title: what is ranked and for which period. |
| `highlightId?` | `string` | `""` | The id of the current user's row: tinted, with a "You" chip. When it falls outside the limit it is pinned as an extra last row after a gap. |
| `valueFormat?` | `LeaderboardValueFormat = "number" \| "compact" \| "currency" \| "percent"` | `"number"` | How values print, through the chart kit with the locale: "48,921", "49K", "$48,921", "12.4%". |
| `currency?` | `string` | `"USD"` | ISO 4217 code for valueFormat="currency" (and deltaFormat="currency"), printed with its narrow symbol. An unknown code prints as a plain unit after the number. |
| `decimals?` | `number` | `0` | Fraction digits for values (0 to 6); auto when unset (Auto in the playground): 0 from 100 up, 1 for percent, up to 2 for small numbers. |
| `formatValue?` | `(value: number, item: LeaderboardItem) => string` |  | Full control over the value text, e.g. minutes: v => `${v} min`. The count-up and the spoken sentence use it too. |
| `locale?` | `string` | `""` | Intl locale for values and deltas. Empty uses the UIProvider locale, then the page's language. |
| `actions?` | `ReactNode` |  | Controls at the end of the header: a period picker, a button. |
| `variant?` | `"surface" \| "plain"` | `"surface"` | surface is a raised panel (a list group); plain is bare rows for use inside a card or on a page. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Rows at least 44 / 56 / 64px with 32 / 40 / 48px avatars; a description plus a bar adds a little height. sm never goes below 44px, so it stays a full touch target. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The bar fill. neutral is the foreground ink (black in light mode, white in dark). |
| `color?` | `string` | `""` | Any CSS color or a chart palette slot ("series-3") for the bars; replaces the tone. |
| `showChange?` | `"rank" \| "delta" \| "none"` | `"rank"` | rank: ▲2, ▼1, a gray dash or New, from previousRank. delta: a ghost trend badge from each item's delta. none hides the column. |
| `deltaFormat?` | `LeaderboardDeltaFormat = "percent" \| "number" \| "currency"` | `"percent"` | How deltas print with showChange="delta" (12.4 is +12,4 %). |
| `invertTrend?` | `boolean` | `false` | With showChange="delta": a falling delta is good and turns green (response time, costs). |
| `podium?` | `boolean` | `false` | The top three on steps above the list: 2nd left, 1st in the middle and raised with a crown, 3rd right. The list continues from rank 4. Needs at least three items. |
| `showRank?` | `boolean` | `true` | The rank column (numbers or medals). |
| `medals?` | `boolean` | `true` | Ranks 1 to 3 as gold, silver and bronze discs, in the list and on the podium. |
| `medalColors?` | `[string, string, string]` | `"[\"#E8B923\", \"#A7AFBA\", \"#C27C4E\"]"` | Gold, silver and bronze. The number on each disc turns dark or white by the color's lightness. |
| `showAvatars?` | `boolean` | `true` | The avatar (photo or initials) or the item's icon tile before the name. |
| `showBars?` | `boolean` | `true` | A 4px bar under each name, proportional to the largest value. Hidden when the largest value is zero or less. |
| `dividers?` | `boolean` | `true` | Hairlines between rows. |
| `sort?` | `"desc" \| "asc" \| "none"` | `"desc"` | desc ranks the highest first; asc is for lower-is-better metrics (response time); none keeps the given order. |
| `limit?` | `number` | `5` | Rows before "Show all" (after the podium when it is on). 0 shows every row; a limit larger than the list hides the button. |
| `loading?` | `boolean` | `false` | Skeleton rows with the avatar shape; the header stays. |
| `defaultExpanded?` | `boolean` | `false` | Starts with every row shown. Use expanded / onExpandedChange to control it. |
| `expanded?` | `boolean` |  | Controlled "show all" state. |
| `onExpandedChange?` | `(expanded: boolean) => void` |  | Fires when Show all / Show less is pressed. |
| `emptyState?` | `ReactNode` |  | Shown when items is empty. Defaults to a trophy tile with labels.empty. |
| `animated?` | `boolean` | `true` | Bars grow and values count up the first time the list scrolls into view; rows glide to their new place when the order changes and rank chips pop. Off (and under reduced motion) everything renders final. |
| `onItemClick?` | `(item: LeaderboardItem, rank: number) => void` |  | Makes every row a button with roving focus (Up / Down, Home / End, PageUp / PageDown, Enter). |
| `renderLink?` | `(props: ComponentProps<"a">) => ReactElement` |  | Renders rows whose item has an href, e.g. props => <Link {...props} href={props.href!} />. Defaults to a plain <a>. |
| `labels?` | `Partial<LeaderboardLabels>` |  | showAll(n), showLess, you, new, up(n), down(n), same, increase(text), decrease(text), rank(n), item(rank, name, value, change), empty, loading, list, podium. English defaults; item defaults to rank(n), name, value and change joined with commas. |
| `classNames?` | `Partial<Record<"header" \| "title" \| "metric" \| "actions" \| "podium" \| "list" \| "item" \| "rank" \| "avatar" \| "name" \| "description" \| "value" \| "change" \| "bar" \| "more", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### DEFAULT_LEADERBOARD_LABELS (constant)

The English strings: Show all and Show less, You and New, the spoken rank changes and deltas, each row's sentence, and the empty, loading and list names.

### LeaderboardItem (type)

One row: { id, name, value, avatar?, icon?, description?, previousRank?, delta?, href?, searchName? }; previousRank null marks a new entry.

## Examples

- **Podium**: The top three on steps above the list; every row is a button with roving focus.
- **Products**: Icon tiles instead of avatars, units through formatValue and deltas vs. last month.
- **Lower is better**: sort="asc" for response time; invertTrend turns a falling delta green.
- **Pinned you**: Your row outside the top five stays pinned under a gap until the list expands.
- **Sizes and variants**: sm / md / lg rows, a brand bar, and the plain variant inside a card with a palette color.
- **Loading and empty**: Skeleton rows with the avatar shape, and the empty state.
- **Phone**: At 360px the podium keeps three columns with smaller avatars; rows stay full-size touch targets.

## See also

- [Trend badge](https://wingo-ui.com/components/trend-badge) ([Markdown](https://wingo-ui.com/components/trend-badge.md))
- [Bar chart](https://wingo-ui.com/components/bar-chart) ([Markdown](https://wingo-ui.com/components/bar-chart.md))
- [Funnel chart](https://wingo-ui.com/components/funnel-chart) ([Markdown](https://wingo-ui.com/components/funnel-chart.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Chart kit](https://wingo-ui.com/components/chart-kit) ([Markdown](https://wingo-ui.com/components/chart-kit.md))

---

Canonical: https://wingo-ui.com/components/leaderboard
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
