Sports

Box Score

A game box-score table with teams as rows, scoring periods as columns, and an emphasized totals column.

Install

npx shadcn@latest add @paragon/box-score

box-score.tsx

"use client";

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

export interface BoxScoreTeam {
  code: string;
  name: string;
  /** Points per period, in order. */
  periods: number[];
  color?: string;
}

export interface BoxScoreProps extends React.ComponentProps<"div"> {
  /** Column labels for each period, e.g. ["Q1","Q2","Q3","Q4"]. */
  periodLabels?: string[];
  home?: BoxScoreTeam;
  away?: BoxScoreTeam;
  /** Marks the game final; otherwise the leading total is emphasized. */
  final?: boolean;
}

const DEFAULT_HOME: BoxScoreTeam = {
  code: "BOS",
  name: "Celtics",
  periods: [28, 24, 31, 26],
  color: "#008348",
};
const DEFAULT_AWAY: BoxScoreTeam = {
  code: "LAL",
  name: "Lakers",
  periods: [22, 30, 27, 25],
  color: "#552583",
};

/**
 * A game box-score table: teams as rows, scoring periods as columns, with a
 * bold totals column. Numbers are tabular; the winning total is emphasized.
 * Pure table semantics with a horizontal scroll guard for narrow viewports;
 * no motion.
 */
export function BoxScore({
  periodLabels = ["Q1", "Q2", "Q3", "Q4"],
  home = DEFAULT_HOME,
  away = DEFAULT_AWAY,
  final = true,
  className,
  ...props
}: BoxScoreProps) {
  const rows = [away, home];
  const totals = rows.map((t) => t.periods.reduce((a, b) => a + b, 0));
  const winner = totals[0] === totals[1] ? -1 : totals[0] > totals[1] ? 0 : 1;

  return (
    <div
      data-slot="box-score"
      className={cn(
        "w-full max-w-lg overflow-hidden rounded-xl bg-card text-card-foreground shadow-border",
        className,
      )}
      {...props}
    >
      <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
        <h3 className="text-sm font-semibold">Box Score</h3>
        <span
          className={cn(
            "rounded-md px-2 py-0.5 text-xs font-medium",
            final
              ? "bg-muted text-muted-foreground"
              : "bg-destructive/10 text-destructive",
          )}
        >
          {final ? "Final" : "Live"}
        </span>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead>
            <tr className="text-xs text-muted-foreground">
              <th className="px-4 py-2 text-left font-medium">Team</th>
              {periodLabels.map((p) => (
                <th
                  key={p}
                  className="w-12 px-2 py-2 text-center font-medium tabular-nums"
                >
                  {p}
                </th>
              ))}
              <th className="w-14 px-3 py-2 text-center font-semibold text-foreground">
                T
              </th>
            </tr>
          </thead>
          <tbody>
            {rows.map((team, r) => (
              <tr
                key={team.code}
                className="border-t border-border/70"
              >
                <td className="px-4 py-2.5">
                  <div className="flex items-center gap-2">
                    <span
                      aria-hidden
                      className="size-2.5 shrink-0 rounded-full"
                      style={{ background: team.color ?? "var(--muted-foreground)" }}
                    />
                    <span className="font-semibold">{team.code}</span>
                    <span className="hidden text-muted-foreground @xs:inline">
                      {team.name}
                    </span>
                  </div>
                </td>
                {team.periods.map((pts, i) => (
                  <td
                    key={i}
                    className="px-2 py-2.5 text-center tabular-nums text-muted-foreground"
                  >
                    {pts}
                  </td>
                ))}
                <td
                  className={cn(
                    "px-3 py-2.5 text-center text-base font-bold tabular-nums",
                    winner === r ? "text-foreground" : "text-muted-foreground",
                  )}
                >
                  {totals[r]}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}