Skip to main content

Table themes

A theme sets everything about a table that is not its data: fonts, colours, rules, spacing. sdvtables ships the 20 themes of sdvplotR and gtUtils, each one function call on the builder, so a table can match a site, a publication or a team without any CSS of your own. The theme gallery shows all 20 on the same rows.

One theme​

.theme(name) picks a theme by name. Most themes load their Google Fonts with the table.

AFC, 2024 regular season
TeamWinsLossesPFPANet EPA/play
1523853260.063
1164023010.101
1074253110.108
413309434-0.146
1345253680.190
89345364-0.019
512338404-0.045
413289417
import { STANDINGS } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

const spec = defineTable<(typeof STANDINGS)[number]>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.int("wins"),
c.int("losses"),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
c.colorPills("net_epa", { label: "Net EPA/play", digits: 3, domain: [-0.2, 0.2] }),
])
.theme("midnight")
.title("AFC, 2024 regular season")
.build();
await renderHTMLAsync(spec, STANDINGS);

A team's own colours​

sdvTeam fills the header with a team's primary colour: pass { league, team } as its options. The title takes onColor of the fill, and the subtitle secondaryOn, a blend of the title ink toward the fill that still reads at 4.5:1.

AFC, 2024 regular season
TeamWinsLossesPFPANet EPA/play
1523853260.063
1164023010.101
1074253110.108
413309434-0.146
1345253680.190
89345364-0.019
512338404-0.045
413289417
import { STANDINGS } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

const spec = defineTable<(typeof STANDINGS)[number]>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.int("wins"),
c.int("losses"),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
c.colorPills("net_epa", { label: "Net EPA/play", digits: 3, domain: [-0.2, 0.2] }),
])
.theme("sdvTeam", { options: { league: "nfl", team: "KC" } })
.title("AFC, 2024 regular season")
.build();
await renderHTMLAsync(spec, STANDINGS);
Kansas City, sdvTeamEach row: the header inks sdvTeam would pick for that team
TeamPrimaryTitle InkSubtitle Ink
KC#e31837#ffffff#ffffff
LAC#007bc7#000000#00060a
DEN#002244#ffffff#8090a2
LV#000000#ffffff#808080
BUF#00338d#ffffff#99add1
MIA#008e97#000000#001517
NYJ#003f2d#ffffff#8ca9a0
NE#002244#ffffff#8090a2
import { STANDINGS } from "@sportsdataverse/examples/data";
import { loadLeague, onColor, teamColorsSync } from "@sportsdataverse/sdvplot";
import { defineTable, secondaryOn } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// sdvTeam fills the header with the team's primary colour. The title takes onColor(primary); the subtitle takes
// secondaryOn(primary, title ink): the title ink blended toward the fill as far as it still reads at 4.5:1.
await loadLeague("nfl");
const rows = STANDINGS.map((r) => {
const primary = teamColorsSync("nfl", r.team) ?? "#0B1A33";
const title = onColor(primary);
return { team: r.team, primary, title_ink: title, subtitle_ink: secondaryOn(primary, title) };
});
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [
c.teamColorBar("team", { league: "nfl", barWidth: 8 }),
c.text("primary"),
c.text("title_ink"),
c.text("subtitle_ink"),
])
.theme("sdvTeam", { options: { league: "nfl", team: "KC" } })
.title("Kansas City, sdvTeam")
.subtitle("Each row: the header inks sdvTeam would pick for that team")
.build();
await renderHTMLAsync(spec, rows);

Density​

.theme(name, { density }) scales type and padding from DENSITY: compact, comfortable or social.

density: "compact"body 12px, cell padding 3px
TeamWinsLosses
152
116
107
density: "comfortable"body 14px, cell padding 6px
TeamWinsLosses
152
116
107
density: "social"body 17px, cell padding 9px
TeamWinsLosses
152
116
107
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { DENSITY, type Density, defineTable } from "@sportsdataverse/sdvtables";
import { renderHTMLAsync } from "@sportsdataverse/sdvtables/html";

// .theme(name, { density }) scales type and padding from DENSITY; almanac, scoreboard and terminal default to compact.
const densities: Density[] = ["compact", "comfortable", "social"];
const tables = await Promise.all(
densities.map((density) =>
renderHTMLAsync(
defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.theme("sdv", { density })
.title(`density: "${density}"`)
.subtitle(`body ${DENSITY[density].body}px, cell padding ${DENSITY[density].pad}px`)
.build(),
STANDINGS.slice(0, 3),
),
),
);
tables.join("\n");

Comparing themes​

themePreview(spec, rows, names) renders one table per theme (all 20 when names is left out), for choosing one.

TeamWinsLosses
KC152
BUF134
TeamWinsLosses
KC152
BUF134
TeamWinsLosses
KC152
BUF134
import { defineTable } from "@sportsdataverse/sdvtables";
import { themePreview } from "@sportsdataverse/sdvtables/html";

const rows = [
{ team: "KC", wins: 15, losses: 2 },
{ team: "BUF", wins: 13, losses: 4 },
];
const spec = defineTable<(typeof rows)[number]>()
.columns((c) => [c.text("team"), c.int("wins"), c.int("losses")])
.build();
Object.values(themePreview(spec, rows, ["sdv", "athletic", "midnight"])).join("\n");

sdv

AFC, 2024
TeamWinsLosses
152
116
107

midnight

AFC, 2024
TeamWinsLosses
152
116
107

tufte

AFC, 2024
TeamWinsLosses
152
116
107

scoreboard

AFC, 2024
TeamWinsLosses
152
116
107
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { prepare, themePreview } from "@sportsdataverse/sdvtables/html";

const spec = defineTable<Standing>()
.columns((c) => [c.logo("team", { league: "nfl" }), c.int("wins"), c.int("losses")])
.title("AFC, 2024")
.build();
await prepare(spec);
// One HTML string per theme (all 20 when the list is left out), the first n rows, compact.
const previews = themePreview(spec, STANDINGS, ["sdv", "midnight", "tufte", "scoreboard"], { n: 3 });
Object.entries(previews)
.map(([name, html]) => `<h4>${name}</h4>\n${html}`)
.join("\n");

The registry​

THEMES maps each name in THEME_NAMES to the function that builds it, and resolveTheme picks and calls one. A theme's tokens (TOKEN_KEYS) become CSS custom properties on the table.

{
  "THEME_NAMES": [
    "sdv",
    "sdvTeam",
    "almanac",
    "athletic",
    "booktabs",
    "broadsheet",
    "brutalist",
    "drench",
    "gtutils",
    "kenpom",
    "midnight",
    "ncaa",
    "pl",
    "savant",
    "scoreboard",
    "sofa",
    "swiss",
    "terminal",
    "tier",
    "tufte"
  ],
  "THEMES.midnight is a function": true,
  "TOKEN_KEYS.length": 53,
  "midnight colour tokens": {
    "bg": "#0C0D10",
    "text": "#E8E9ED",
    "muted": "#9498A3",
    "accent": "#5b8def",
    "rule": "#24272E",
    "stripe": "transparent"
  },
  "midnight fonts": [
    "Chivo"
  ],
  "PAL_MIDNIGHT": [
    "#5B8DEF",
    "#3FBF87",
    "#E8E9ED",
    "#9498A3",
    "#24272E"
  ]
}
import { PAL_MIDNIGHT, THEMES, THEME_NAMES, TOKEN_KEYS, resolveTheme } from "@sportsdataverse/sdvtables";

// A theme is a function of { name, density, options } (THEMES[name]); resolveTheme picks and calls it.
// Its tokens (TOKEN_KEYS) become CSS custom properties on the table.
const midnight = resolveTheme({ name: "midnight", density: "comfortable" });
const { bg, text, muted, accent, rule, stripe } = midnight.tokens;

{
THEME_NAMES,
"THEMES.midnight is a function": typeof THEMES.midnight === "function",
"TOKEN_KEYS.length": TOKEN_KEYS.length,
"midnight colour tokens": { bg, text, muted, accent, rule, stripe },
"midnight fonts": midnight.fonts.map((f) => f.family),
PAL_MIDNIGHT,
};

In the gallery: Every theme and Theme API.