Sports

Player Card

A player stat card with a headshot placeholder, team accent band, and a season stat grid in tabular figures.

Install

npx shadcn@latest add @paragon/player-card

player-card.tsx

"use client";

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

export interface PlayerStat {
  label: string;
  value: string;
}

export interface PlayerCardProps extends React.ComponentProps<"div"> {
  name?: string;
  /** e.g. "Forward · #0". */
  role?: string;
  team?: string;
  teamColor?: string;
  /** Two initials for the headshot placeholder. */
  initials?: string;
  /** Season line, e.g. "2025–26 · 64 GP". */
  season?: string;
  stats?: PlayerStat[];
}

const DEFAULT_STATS: PlayerStat[] = [
  { label: "PPG", value: "27.4" },
  { label: "RPG", value: "8.1" },
  { label: "APG", value: "4.9" },
  { label: "FG%", value: "47.2" },
];

/**
 * A player stat card: a gradient headshot placeholder with initials, name and
 * role, a team accent bar, and a season stat grid in tabular figures. The
 * accent color threads through the header band and stat separators. Static —
 * no motion; reads cleanly in both themes.
 */
export function PlayerCard({
  name = "Jayson Tatum",
  role = "Forward · #0",
  team = "Boston Celtics",
  teamColor = "#008348",
  initials = "JT",
  season = "2025–26 · 64 GP",
  stats = DEFAULT_STATS,
  className,
  ...props
}: PlayerCardProps) {
  return (
    <div
      data-slot="player-card"
      className={cn(
        "w-full max-w-xs overflow-hidden rounded-xl bg-card text-card-foreground shadow-border",
        className,
      )}
      {...props}
    >
      <div
        className="relative h-20"
        style={{
          background: `linear-gradient(120deg, ${teamColor}, color-mix(in oklch, ${teamColor} 45%, black))`,
        }}
      >
        <span
          aria-hidden
          className="absolute -bottom-8 left-4 grid size-20 place-items-center rounded-full border-4 border-card text-xl font-bold text-white/90"
          style={{
            background: `linear-gradient(135deg, color-mix(in oklch, ${teamColor} 70%, white), ${teamColor})`,
          }}
        >
          {initials}
        </span>
      </div>

      <div className="px-4 pt-10 pb-4">
        <h3 className="text-base font-bold leading-tight">{name}</h3>
        <p className="text-xs text-muted-foreground">{role}</p>
        <div className="mt-1.5 flex items-center gap-1.5">
          <span
            aria-hidden
            className="size-2 rounded-full"
            style={{ background: teamColor }}
          />
          <span className="text-xs font-medium text-muted-foreground">
            {team}
          </span>
        </div>

        <div className="mt-4 grid grid-cols-4 divide-x divide-border rounded-lg bg-muted/50 py-2.5">
          {stats.map((s) => (
            <div key={s.label} className="px-1 text-center">
              <div className="text-lg font-bold tabular-nums leading-none">
                {s.value}
              </div>
              <div className="mt-1 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
                {s.label}
              </div>
            </div>
          ))}
        </div>

        <p className="mt-3 text-center text-[11px] tabular-nums text-muted-foreground">
          {season}
        </p>
      </div>
    </div>
  );
}