Skip to content
edmi-ui

Leaderboard Podium

Top-three podium cards with rank badges and avatars; first place stands taller in the middle.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
  1. #2NO
    noah
    $31.7K · 265 holders
  2. #1DE
    dewi
    $49.2K · 412 holders
  3. #3SA
    sarah
    $18.9K · 140 holders
  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders
  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders
Terminal window
npx shadcn@latest add @edmi-ui/leaderboard-podium
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/leaderboard-podium
Terminal window
yarn dlx shadcn@latest add @edmi-ui/leaderboard-podium
Terminal window
bunx shadcn@latest add @edmi-ui/leaderboard-podium

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/leaderboard-podium
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/leaderboard-podium
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/leaderboard-podium
Terminal window
bunx shadcn-vue@latest add @edmi-ui/leaderboard-podium

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/leaderboard-podium.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
LeaderboardPodium,
} from "@/components/leaderboard-podium/leaderboard-podium";
<LeaderboardPodium
entries={[
{ rank: 1, name: "dewi", meta: "$49.2K · 412 holders" },
{ rank: 2, name: "noah", meta: "$31.7K · 265 holders" },
{ rank: 3, name: "sarah", meta: "$18.9K · 140 holders" },
]}
/>
<script setup lang="ts">
import { LeaderboardPodium } from "@/components/ui/leaderboard-podium";
</script>
<template>
<LeaderboardPodium :entries="[...entries]" />
</template>
<script lang="ts">
import { LeaderboardPodium } from "$lib/components/ui/leaderboard-podium";
</script>
<LeaderboardPodium />
registry/blocks/leaderboard-podium/leaderboard-podium.tsx
import { cn } from "cn";
import type * as React from "react";
import { AllocationBar } from "@/registry/edmi/components/allocation-bar";
import { Sparkline } from "@/registry/edmi/components/index-row";
import { Avatar, AvatarFallback, AvatarImage } from "@/registry/edmi/ui/avatar";
import { Badge } from "@/registry/edmi/ui/badge";
import { Card } from "@/registry/edmi/ui/card";
import type { Elevation } from "@/registry/edmi/ui/elevation";
type PodiumAllocation = {
label: string;
/** Weight in percent (bars are proportional). */
value: number;
/** Any CSS color; defaults cycle `--chart-1…5`. */
color?: string;
};
type PodiumEntry = {
rank: 1 | 2 | 3;
name: string;
/** Mono caption under the name, e.g. `$49.2K · 412 members`. */
meta?: string;
image?: string;
/** Avatar initials; defaults to the first two letters of `name`. */
initials?: string;
href?: string;
/** ✦ `variant="cards"`: mono ticker under the name. */
symbol?: string;
/** ✦ `variant="cards"`: mono owner, top right of the card. */
owner?: string;
/** ✦ `variant="cards"`: signed headline percentage, e.g. `+1.12%`. */
change?: string;
/** ✦ `variant="cards"`: series for the sparkline. */
spark?: number[];
/** ✦ `variant="cards"`: weights bar with legend. */
allocation?: PodiumAllocation[];
/** ✦ `variant="cards"`: footer stat, labelled by `valueLabel`. */
value?: string;
/** ✦ `variant="cards"`: footer stat, labelled by `countLabel`. */
count?: string | number;
};
const isDown = (s: string) => /^[-−–]/.test(s.trim());
// `podium` (default): top three, #1 raised in the middle (warning badge), #2 left and #3 right sit lower.
// `cards` ✦: three equal stat cards in rank order (leaderboard board): owner, name, headline change,
// sparkline, allocation bar and value / count stats.
function LeaderboardPodium({
className,
entries,
elevation,
variant = "podium",
valueLabel = "Value",
countLabel = "Count",
...props
}: Omit<React.ComponentProps<"ol">, "children"> & {
entries: PodiumEntry[];
/** ✦ depth of the cards (forwarded to each Card). */
elevation?: Elevation;
/** ✦ `podium` (default) or `cards`. */
variant?: "podium" | "cards";
/** ✦ `cards`: label of the footer `value` stat. */
valueLabel?: React.ReactNode;
/** ✦ `cards`: label of the footer `count` stat. */
countLabel?: React.ReactNode;
}) {
const by = (r: number) => entries.find((e) => e.rank === r);
if (variant === "cards") {
const sorted = [by(1), by(2), by(3)].filter((e): e is PodiumEntry => !!e);
return (
<ol
data-slot="leaderboard-podium"
data-variant="cards"
className={cn("grid gap-4 lg:grid-cols-3", className)}
{...props}
>
{sorted.map((e) => {
const down = e.change ? isDown(e.change) : false;
return (
<li key={e.rank}>
<Card elevation={elevation} className="h-full gap-0 p-6">
<div className="flex items-center justify-between text-[13px]">
<span className="font-medium">No. {e.rank}</span>
{e.owner ? (
<span className="font-mono text-xs text-muted-foreground">
{e.owner}
</span>
) : null}
</div>
<div className="mt-3 flex items-center gap-3">
<Avatar className="size-11 rounded-xl after:rounded-xl">
{e.image ? (
<AvatarImage
src={e.image}
alt=""
className="rounded-xl"
/>
) : null}
<AvatarFallback className="rounded-xl bg-muted font-mono text-sm text-foreground-2">
{(e.initials ?? e.name.slice(0, 2)).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
{e.href ? (
<a
href={e.href}
className="text-lg font-medium hover:underline"
>
{e.name}
</a>
) : (
<div className="text-lg font-medium">{e.name}</div>
)}
{e.symbol ? (
<div className="font-mono text-xs text-muted-foreground">
{e.symbol}
</div>
) : null}
</div>
</div>
{e.change ? (
<div className="mt-4 flex items-end justify-between gap-3">
<span
className={cn(
"text-[30px] leading-none tracking-[-0.8px]",
down ? "text-destructive-text" : "text-brand-text",
)}
>
{e.change}
</span>
{e.spark ? (
<Sparkline
data={e.spark}
tone={down ? "down" : "up"}
width={150}
height={34}
className="max-w-[45%]"
/>
) : null}
</div>
) : null}
{e.allocation?.length ? (
<AllocationBar
segments={e.allocation}
size="sm"
className="mt-4"
/>
) : null}
{e.value !== undefined || e.count !== undefined ? (
<div className="mt-4 flex items-center gap-4 border-t border-border-2 pt-3.5 text-[13px] text-muted-foreground">
{e.value !== undefined ? (
<span>
{valueLabel}{" "}
<b className="font-mono font-medium text-foreground">
{e.value}
</b>
</span>
) : null}
{e.count !== undefined ? (
<span>
{countLabel}{" "}
<b className="font-mono font-medium text-foreground">
{e.count}
</b>
</span>
) : null}
</div>
) : null}
</Card>
</li>
);
})}
</ol>
);
}
const ordered = [by(2), by(1), by(3)].filter((e): e is PodiumEntry => !!e);
return (
<ol
data-slot="leaderboard-podium"
className={cn("flex items-start gap-3", className)}
{...props}
>
{ordered.map((e) => (
<li key={e.rank} className={cn("w-[200px]", e.rank !== 1 && "mt-6")}>
<Card
size="sm"
elevation={elevation}
className="items-center gap-0 p-[18px] text-center"
>
<Badge
shape="number"
variant={e.rank === 1 ? "warning" : "secondary"}
className="rounded-full"
>
#{e.rank}
</Badge>
<Avatar className="mt-3 size-11">
{e.image ? <AvatarImage src={e.image} alt="" /> : null}
<AvatarFallback className="bg-linear-to-br from-info to-brand text-sm text-white">
{(e.initials ?? e.name.slice(0, 2)).toUpperCase()}
</AvatarFallback>
</Avatar>
{e.href ? (
<a href={e.href} className="mt-2.5 font-semibold hover:underline">
{e.name}
</a>
) : (
<div className="mt-2.5 font-semibold">{e.name}</div>
)}
{e.meta ? (
<div className="mt-0.5 font-mono text-xs text-muted-foreground">
{e.meta}
</div>
) : null}
</Card>
</li>
))}
</ol>
);
}
export type { PodiumAllocation, PodiumEntry };
export { LeaderboardPodium };
registry/ui/leaderboard-podium/LeaderboardPodium.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { PodiumEntry } from '.'
import { computed } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { AllocationBar } from '@/registry/edmi/ui/allocation-bar'
import { Avatar, AvatarFallback, AvatarImage } from '@/registry/edmi/ui/avatar'
import { Badge } from '@/registry/edmi/ui/badge'
import { Card } from '@/registry/edmi/ui/card'
import type { Elevation } from '@/registry/edmi/ui/elevation'
import { Sparkline } from '@/registry/edmi/ui/index-row'
const props = withDefaults(defineProps<{
/** ✦ depth of the cards, forwarded to each Card */
elevation?: Elevation
/** ✦ `podium` (default) or `cards` */
variant?: 'podium' | 'cards'
/** ✦ `cards`: label of the footer `value` stat */
valueLabel?: string
/** ✦ `cards`: label of the footer `count` stat */
countLabel?: string
entries: PodiumEntry[]
class?: HTMLAttributes['class']
}>(), {
variant: 'podium',
valueLabel: 'Value',
countLabel: 'Count',
})
const isDown = (s: string) => /^[-−–]/.test(s.trim())
// `podium` (default): #1 raised in the middle (warning badge), #2 left and #3 right sit lower.
// `cards` ✦: three equal stat cards in rank order (leaderboard board).
const ordered = computed(() => {
const by = (r: number) => props.entries.find(e => e.rank === r)
const order = props.variant === 'cards' ? [1, 2, 3] : [2, 1, 3]
return order.map(by).filter((e): e is PodiumEntry => !!e)
})
</script>
<template>
<ol
v-if="variant === 'cards'"
data-slot="leaderboard-podium"
data-variant="cards"
:class="cn('grid gap-4 lg:grid-cols-3', props.class)"
>
<li v-for="e in ordered" :key="e.rank">
<Card :elevation="elevation" class="h-full gap-0 p-6">
<div class="flex items-center justify-between text-[13px]">
<span class="font-medium">No. {{ e.rank }}</span>
<span v-if="e.owner" class="font-mono text-xs text-muted-foreground">{{ e.owner }}</span>
</div>
<div class="mt-3 flex items-center gap-3">
<Avatar class="size-11 rounded-xl after:rounded-xl">
<AvatarImage v-if="e.image" :src="e.image" alt="" class="rounded-xl" />
<AvatarFallback class="rounded-xl bg-muted font-mono text-sm text-foreground-2">
{{ (e.initials ?? e.name.slice(0, 2)).toUpperCase() }}
</AvatarFallback>
</Avatar>
<div>
<a v-if="e.href" :href="e.href" class="text-lg font-medium hover:underline">{{ e.name }}</a>
<div v-else class="text-lg font-medium">
{{ e.name }}
</div>
<div v-if="e.symbol" class="font-mono text-xs text-muted-foreground">
{{ e.symbol }}
</div>
</div>
</div>
<div v-if="e.change" class="mt-4 flex items-end justify-between gap-3">
<span :class="cn('text-[30px] leading-none tracking-[-0.8px]', isDown(e.change) ? 'text-destructive-text' : 'text-brand-text')">
{{ e.change }}
</span>
<Sparkline
v-if="e.spark"
:data="e.spark"
:tone="isDown(e.change) ? 'down' : 'up'"
:width="150"
:height="34"
class="max-w-[45%]"
/>
</div>
<AllocationBar v-if="e.allocation?.length" :segments="e.allocation" size="sm" class="mt-4" />
<div
v-if="e.value !== undefined || e.count !== undefined"
class="mt-4 flex items-center gap-4 border-t border-border-2 pt-3.5 text-[13px] text-muted-foreground"
>
<span v-if="e.value !== undefined">{{ valueLabel }} <b class="font-mono font-medium text-foreground">{{ e.value }}</b></span>
<span v-if="e.count !== undefined">{{ countLabel }} <b class="font-mono font-medium text-foreground">{{ e.count }}</b></span>
</div>
</Card>
</li>
</ol>
<ol v-else data-slot="leaderboard-podium" :class="cn('flex items-start gap-3', props.class)">
<li v-for="e in ordered" :key="e.rank" :class="cn('w-[200px]', e.rank !== 1 && 'mt-6')">
<Card :elevation="elevation" size="sm" class="items-center gap-0 p-[18px] text-center">
<Badge shape="number" :variant="e.rank === 1 ? 'warning' : 'secondary'" class="rounded-full">
#{{ e.rank }}
</Badge>
<Avatar class="mt-3 size-11">
<AvatarImage v-if="e.image" :src="e.image" alt="" />
<AvatarFallback class="bg-linear-to-br from-info to-brand text-sm text-white">
{{ (e.initials ?? e.name.slice(0, 2)).toUpperCase() }}
</AvatarFallback>
</Avatar>
<a v-if="e.href" :href="e.href" class="mt-2.5 font-semibold hover:underline">{{ e.name }}</a>
<div v-else class="mt-2.5 font-semibold">
{{ e.name }}
</div>
<div v-if="e.meta" class="mt-0.5 font-mono text-xs text-muted-foreground">
{{ e.meta }}
</div>
</Card>
</li>
</ol>
</template>
registry/ui/leaderboard-podium/index.ts
export type PodiumAllocation = {
label: string;
/** Weight in percent (bars are proportional). */
value: number;
/** Any CSS color; defaults cycle `--chart-1…5`. */
color?: string;
};
export type PodiumEntry = {
rank: 1 | 2 | 3;
name: string;
/** Mono caption under the name, e.g. `$49.2K · 412 members`. */
meta?: string;
image?: string;
/** Avatar initials; defaults to the first two letters of `name`. */
initials?: string;
href?: string;
/** ✦ `variant="cards"`: mono ticker under the name. */
symbol?: string;
/** ✦ `variant="cards"`: mono owner, top right of the card. */
owner?: string;
/** ✦ `variant="cards"`: signed headline percentage, e.g. `+1.12%`. */
change?: string;
/** ✦ `variant="cards"`: series for the sparkline. */
spark?: number[];
/** ✦ `variant="cards"`: weights bar with legend. */
allocation?: PodiumAllocation[];
/** ✦ `variant="cards"`: footer stat, labelled by `valueLabel`. */
value?: string;
/** ✦ `variant="cards"`: footer stat, labelled by `countLabel`. */
count?: string | number;
};
export { default as LeaderboardPodium } from "./LeaderboardPodium.vue";
src/lib/registry/ui/leaderboard-podium/index.ts
export {
default as LeaderboardPodium,
type PodiumAllocation,
type PodiumEntry,
} from "./leaderboard-podium.svelte";
src/lib/registry/ui/leaderboard-podium/leaderboard-podium.svelte
<script lang="ts" module>
export type PodiumAllocation = {
label: string;
/** Weight in percent (bars are proportional). */
value: number;
/** Any CSS color; defaults cycle `--chart-1…5`. */
color?: string;
};
export type PodiumEntry = {
rank: 1 | 2 | 3;
name: string;
/** Mono caption under the name, e.g. `$49.2K · 412 members`. */
meta?: string;
image?: string;
/** Avatar initials; defaults to the first two letters of `name`. */
initials?: string;
href?: string;
/** ✦ `variant="cards"`: mono ticker under the name. */
symbol?: string;
/** ✦ `variant="cards"`: mono owner, top right of the card. */
owner?: string;
/** ✦ `variant="cards"`: signed headline percentage, e.g. `+1.12%`. */
change?: string;
/** ✦ `variant="cards"`: series for the sparkline. */
spark?: number[];
/** ✦ `variant="cards"`: weights bar with legend. */
allocation?: PodiumAllocation[];
/** ✦ `variant="cards"`: footer stat, labelled by `valueLabel`. */
value?: string;
/** ✦ `variant="cards"`: footer stat, labelled by `countLabel`. */
count?: string | number;
};
</script>
<script lang="ts">
import { AllocationBar } from "$lib/registry/ui/allocation-bar/index.js";
import { Avatar, AvatarFallback, AvatarImage } from "$lib/registry/ui/avatar/index.js";
import { Badge } from "$lib/registry/ui/badge/index.js";
import { Card } from "$lib/registry/ui/card/index.js";
import { Sparkline } from "$lib/registry/ui/index-row/index.js";
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLOlAttributes } from "svelte/elements";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
entries,
elevation = "auto",
variant = "podium",
valueLabel = "Value",
countLabel = "Count",
...restProps
}: WithElementRef<Omit<HTMLOlAttributes, "children">, HTMLOListElement> & {
entries: PodiumEntry[];
/** ✦ depth of the cards (forwarded to each Card). */
elevation?: Elevation;
/** ✦ `podium` (default) or `cards`. */
variant?: "podium" | "cards";
/** ✦ `cards`: label of the footer `value` stat. */
valueLabel?: string | Snippet;
/** ✦ `cards`: label of the footer `count` stat. */
countLabel?: string | Snippet;
} = $props();
const isDown = (s: string) => /^[-−–]/.test(s.trim());
// `podium` (default): #1 raised in the middle (warning badge), #2 left and #3 right sit lower.
// `cards` ✦: three equal stat cards in rank order (leaderboard board).
const ordered = $derived(
(variant === "cards" ? ([1, 2, 3] as const) : ([2, 1, 3] as const))
.map((r) => entries.find((e) => e.rank === r))
.filter((e): e is PodiumEntry => !!e)
);
</script>
{#snippet text(v: string | Snippet)}
{#if typeof v === "function"}{@render v()}{:else}{v}{/if}
{/snippet}
{#if variant === "cards"}
<ol
bind:this={ref}
data-slot="leaderboard-podium"
data-variant="cards"
class={cn("grid gap-4 lg:grid-cols-3", className)}
{...restProps}
>
{#each ordered as e (e.rank)}
{@const down = e.change ? isDown(e.change) : false}
<li>
<Card {elevation} class="h-full gap-0 p-6">
<div class="flex items-center justify-between text-[13px]">
<span class="font-medium">No. {e.rank}</span>
{#if e.owner}<span class="font-mono text-xs text-muted-foreground">{e.owner}</span>{/if}
</div>
<div class="mt-3 flex items-center gap-3">
<Avatar class="size-11 rounded-xl after:rounded-xl">
{#if e.image}<AvatarImage src={e.image} alt="" class="rounded-xl" />{/if}
<AvatarFallback class="rounded-xl bg-muted font-mono text-sm text-foreground-2">
{(e.initials ?? e.name.slice(0, 2)).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
{#if e.href}
<a href={e.href} class="text-lg font-medium hover:underline">{e.name}</a>
{:else}
<div class="text-lg font-medium">{e.name}</div>
{/if}
{#if e.symbol}<div class="font-mono text-xs text-muted-foreground">{e.symbol}</div>{/if}
</div>
</div>
{#if e.change}
<div class="mt-4 flex items-end justify-between gap-3">
<span class={cn("text-[30px] leading-none tracking-[-0.8px]", down ? "text-destructive-text" : "text-brand-text")}>
{e.change}
</span>
{#if e.spark}
<Sparkline data={e.spark} tone={down ? "down" : "up"} width={150} height={34} class="max-w-[45%]" />
{/if}
</div>
{/if}
{#if e.allocation?.length}
<AllocationBar segments={e.allocation} size="sm" class="mt-4" />
{/if}
{#if e.value !== undefined || e.count !== undefined}
<div class="mt-4 flex items-center gap-4 border-t border-border-2 pt-3.5 text-[13px] text-muted-foreground">
{#if e.value !== undefined}<span>{@render text(valueLabel)} <b class="font-mono font-medium text-foreground">{e.value}</b></span>{/if}
{#if e.count !== undefined}<span>{@render text(countLabel)} <b class="font-mono font-medium text-foreground">{e.count}</b></span>{/if}
</div>
{/if}
</Card>
</li>
{/each}
</ol>
{:else}
<ol bind:this={ref} data-slot="leaderboard-podium" class={cn("flex items-start gap-3", className)} {...restProps}>
{#each ordered as e (e.rank)}
<li class={cn("w-[200px]", e.rank !== 1 && "mt-6")}>
<Card size="sm" {elevation} class="items-center gap-0 p-[18px] text-center">
<Badge shape="number" variant={e.rank === 1 ? "warning" : "secondary"} class="rounded-full">#{e.rank}</Badge>
<Avatar class="mt-3 size-11">
{#if e.image}<AvatarImage src={e.image} alt="" />{/if}
<AvatarFallback class="bg-linear-to-br from-info to-brand text-sm text-white">
{(e.initials ?? e.name.slice(0, 2)).toUpperCase()}
</AvatarFallback>
</Avatar>
{#if e.href}
<a href={e.href} class="mt-2.5 font-semibold hover:underline">{e.name}</a>
{:else}
<div class="mt-2.5 font-semibold">{e.name}</div>
{/if}
{#if e.meta}<div class="mt-0.5 font-mono text-xs text-muted-foreground">{e.meta}</div>{/if}
</Card>
</li>
{/each}
</ol>
{/if}

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation is forwarded to every card: sunken (-1) a soft inset well, raised (+1) the bevel, floating (+2) the bevel plus one soft drop. In a layered ElevationProvider the card resolves to its role default (raised).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

  1. #2NO
    noah
    $31.7K · 265 holders
  2. #1DE
    dewi
    $49.2K · 412 holders
  3. #3SA
    sarah
    $18.9K · 140 holders

Raised (+1)

  1. #2NO
    noah
    $31.7K · 265 holders
  2. #1DE
    dewi
    $49.2K · 412 holders
  3. #3SA
    sarah
    $18.9K · 140 holders

Floating (+2)

  1. #2NO
    noah
    $31.7K · 265 holders
  2. #1DE
    dewi
    $49.2K · 412 holders
  3. #3SA
    sarah
    $18.9K · 140 holders

Flat (0)

  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders

Raised (+1)

  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders

Floating (+2)

  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders

Flat (0)

  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders

Raised (+1)

  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders

Floating (+2)

  1. #2
    NO
    noah
    $31.7K · 265 holders
  2. #1
    DE
    dewi
    $49.2K · 412 holders
  3. #3
    SA
    sarah
    $18.9K · 140 holders
Part Prop Type Default
LeaderboardPodium entries { rank: 1 | 2 | 3, name, meta?, image?, initials?, href? }[] required
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
variant ✦ "podium" | "cards" "podium"
valueLabel / countLabel ✦ label of the cards footer stats (Svelte string | Snippet) "Value" / "Count"
entries[] (cards) ✦ symbol, owner, change, spark, allocation, value, count cards variant fields; spark renders the Sparkline of Index Row and allocation the Allocation Bar (size="sm")

Order is fixed visually (2, 1, 3) whatever the array order.

variant="cards" is the leaderboard top three: three equal cards in rank order with owner, name and ticker, headline change with sparkline, an allocation bar and two footer stats (value, count, labelled by valueLabel / countLabel). meta is not needed there.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
  1. No. 1GbFK…ZUWS
    MA
    Mag Four Tilt
    MAGT
    +1.12%
    • AAPLx30%
    • NVDAx40%
    • TSLAx30%
    AUM $120.1KHolders 2
  2. No. 28FcD…cL9y
    AI
    AI Frontier
    AIFR
    +0.84%
    • NVDAx55%
    • MSFTx45%
    AUM $82.4KHolders 2
  3. No. 37Ge1…kXSq
    DE
    Defense & Space
    DFSP
    -2.09%
    • SPACEX-pre60%
    • TSLAx40%
    AUM $29KHolders 1
  1. No. 1GbFK…ZUWS
    MA
    Mag Four Tilt
    MAGT
    +1.12%
    • AAPLx 30%
    • NVDAx 40%
    • TSLAx 30%
    AUM $120.1KHolders 2
  2. No. 28FcD…cL9y
    AI
    AI Frontier
    AIFR
    +0.84%
    • NVDAx 55%
    • MSFTx 45%
    AUM $82.4KHolders 2
  3. No. 37Ge1…kXSq
    DE
    Defense & Space
    DFSP
    -2.09%
    • SPACEX-pre 60%
    • TSLAx 40%
    AUM $29KHolders 1
  1. No. 1 GbFK…ZUWS
    MA
    Mag Four Tilt
    MAGT
    +1.12%
    • AAPLx 30%
    • NVDAx 40%
    • TSLAx 30%
    AUM $120.1K Holders 2
  2. No. 2 8FcD…cL9y
    AI
    AI Frontier
    AIFR
    +0.84%
    • NVDAx 55%
    • MSFTx 45%
    AUM $82.4K Holders 2
  3. No. 3 7Ge1…kXSq
    DE
    Defense & Space
    DFSP
    -2.09%
    • SPACEX-pre 60%
    • TSLAx 40%
    AUM $29K Holders 1
  • #1 gets the warning badge, #2/#3 the secondary badge.