
SportsDataverse Table Themes
Saiem Gilani
Source: vignettes/sdv-table-themes.Rmd
sdv-table-themes.RmdOn this page
sdvplotR puts team logos, headshots and colors inside gt
tables. The table around them still needs a look, and two themes are
built for that:
-
gt_theme_sdv()is the SportsDataverse house table, in light or dark. -
gt_theme_sdv_team()dresses the same table in one team’s colors.
Both follow the same rules. Titles and column labels are set in Chivo, and the body in Lato with tabular figures, so digits line up down a column. Column labels keep the case you give them. The one accent is a line under the column labels, so logos and team colors stay the loudest thing in the table.
The data
The final 2025 AFC West standings.
nflseedR::nfl_standings() computes them from nflverse’s
schedule, read with nflreadr, NFL tiebreakers included, and
sdv_teams() adds each team’s full name. The logos come from
gt_sdv_logos(), which resolves the abbreviations through
sdv_resolve().
nfl_teams <- sdv_teams("nfl")
standings <- nflseedR::nfl_standings(nflreadr::load_schedules(2025), verbosity = "NONE") |>
subset(division == "AFC West")
standings <- standings[order(standings$div_rank), ]
standings <- data.frame(
team = standings$team,
name = nfl_teams$name[match(standings$team, nfl_teams$abbr)],
w = standings$true_wins,
l = standings$losses,
pct = standings$win_pct
)
standings_table <- gt(standings) |>
gt_sdv_logos(columns = "team", league = "nfl", height = 26) |>
cols_label(team = "", name = "Team", w = "W", l = "L", pct = "Win pct") |>
# win percentage without the leading zero, as box scores print it (.647)
fmt(columns = "pct", fns = function(x) sub("^0", "", sprintf("%.3f", x))) |>
tab_header("AFC West standings", "2025 regular season") |>
tab_source_note("Data: nflverse via nflreadr and nflseedR")The house theme
standings_table |> gt_theme_sdv()| AFC West standings | ||||
| 2025 regular season | ||||
| Team | W | L | Win pct | |
|---|---|---|---|---|
![]() |
Denver Broncos | 14 | 3 | .824 |
![]() |
Los Angeles Chargers | 11 | 6 | .647 |
![]() |
Kansas City Chiefs | 6 | 11 | .353 |
![]() |
Las Vegas Raiders | 3 | 14 | .176 |
| Data: nflverse via nflreadr and nflseedR | ||||
The line under the column labels is the SportsDataverse gradient, the same one in the package logo. It is the only decoration, drawn once across the whole table rather than per cell.
Dark
style = "dark" sets the same table on the
SportsDataverse navy. It matches the dark mode of this site, and suits
posts that sit on a dark background.
standings_table |> gt_theme_sdv(style = "dark")| AFC West standings | ||||
| 2025 regular season | ||||
| Team | W | L | Win pct | |
|---|---|---|---|---|
![]() |
Denver Broncos | 14 | 3 | .824 |
![]() |
Los Angeles Chargers | 11 | 6 | .647 |
![]() |
Kansas City Chiefs | 6 | 11 | .353 |
![]() |
Las Vegas Raiders | 3 | 14 | .176 |
| Data: nflverse via nflreadr and nflseedR | ||||
A team’s colors
gt_theme_sdv_team() takes a team and a sport. It uses
the colors sdv_team_colors() returns: the title block is
filled with the primary color, the line under the labels uses the
secondary color, and the labels are set in the primary color. Any
abbreviation, alias or full name sdv_resolve() accepts
works as team. Here the division winner’s yardage leaders,
from nflverse’s season player stats, wear the winner’s colors.
winner <- standings$team[1]
players <- nflreadr::load_player_stats(2025, summary_level = "reg")
players <- players[players$recent_team == winner, ]
leader <- function(stat, label) {
top <- players[which.max(players[[stat]]), ]
data.frame(player = top$player_display_name, stat = label, yards = top[[stat]])
}
rbind(
leader("passing_yards", "Passing"),
leader("receiving_yards", "Receiving"),
leader("rushing_yards", "Rushing")
) |>
gt() |>
cols_label(player = "Player", stat = "", yards = "Yards") |>
fmt_number(columns = "yards", decimals = 0) |>
tab_header(
paste(nfl_teams$short_name[nfl_teams$abbr == winner], "yardage leaders"),
"2025 regular season"
) |>
gt_theme_sdv_team(team = winner, league = "nfl")| Broncos yardage leaders | ||
| 2025 regular season | ||
| Player | Yards | |
|---|---|---|
| Bo Nix | Passing | 3,931 |
| Courtland Sutton | Receiving | 1,017 |
| J.K. Dobbins | Rushing | 772 |
Colors that would not read
Team colors are chosen for jerseys, not for text on a white table, so the theme checks contrast rather than using the colors as they come:
- Title text on the filled block is black or white, whichever contrasts more with the primary color.
- A secondary color too pale to see on white (Duke’s is white) gives way to the primary color for the line.
- A primary color too light to read as text (the Saints’ gold) gives way to the SportsDataverse navy for the column labels.
Each table below shows the team’s own colors as
sdv_team_colors() returns them, dressed in those
colors.
color_record <- function(league, team) {
data.frame(
team = team,
primary = unname(sdv_team_colors(league, team, "primary")),
secondary = unname(sdv_team_colors(league, team, "secondary"))
)
}
color_record("mbb", "DUKE") |>
gt() |>
tab_header("Duke Blue Devils", "A white secondary: the line takes the primary") |>
gt_theme_sdv_team(team = "DUKE", league = "mbb")| Duke Blue Devils | ||
| A white secondary: the line takes the primary | ||
| team | primary | secondary |
|---|---|---|
| DUKE | #00539b | #ffffff |
color_record("nfl", "NO") |>
gt() |>
tab_header("New Orleans Saints", "A gold primary: the labels go navy") |>
gt_theme_sdv_team(team = "NO", league = "nfl")| New Orleans Saints | ||
| A gold primary: the labels go navy | ||
| team | primary | secondary |
|---|---|---|
| NO | #d3bc8d | #000000 |
With team = NULL the table wears the SportsDataverse
navy and cyan.
Sizing for where it goes
Every theme takes density. "comfortable" is
the default, "compact" tightens type and row padding for
long tables, and "social" scales both up to the size
gt_save_crop() and gt_social_crop() export
at.
standings_table |> gt_theme_sdv(density = "compact")| AFC West standings | ||||
| 2025 regular season | ||||
| Team | W | L | Win pct | |
|---|---|---|---|---|
![]() |
Denver Broncos | 14 | 3 | .824 |
![]() |
Los Angeles Chargers | 11 | 6 | .647 |
![]() |
Kansas City Chiefs | 6 | 11 | .353 |
![]() |
Las Vegas Raiders | 3 | 14 | .176 |
| Data: nflverse via nflreadr and nflseedR | ||||
Changing one thing
Anything in ... goes to gt::tab_options()
after the theme’s own options, so it overrides them. Center the heading
and keep everything else:
standings_table |> gt_theme_sdv(heading.align = "center")| AFC West standings | ||||
| 2025 regular season | ||||
| Team | W | L | Win pct | |
|---|---|---|---|---|
![]() |
Denver Broncos | 14 | 3 | .824 |
![]() |
Los Angeles Chargers | 11 | 6 | .647 |
![]() |
Kansas City Chiefs | 6 | 11 | .353 |
![]() |
Las Vegas Raiders | 3 | 14 | .176 |
| Data: nflverse via nflreadr and nflseedR | ||||
Saving an image
theme_bg records the background each theme paints. Pass
it to gt_save_crop() so the padding around the saved image
matches the table instead of framing it in white. Saving drives a
headless Chrome through webshot2, so this chunk is not run
here.
bg <- theme_bg$bg[theme_bg$theme == "gt_theme_sdv" & theme_bg$has_style == "dark"]
standings_table |>
gt_theme_sdv(style = "dark", density = "social") |>
gt_save_crop("afc-west.png", bg = bg)To see these next to every other theme that needs only the table, on
your own data, run gt_theme_preview(standings).
Related SportsDataverse packages
- sdvplot - the Python plotting package
- sdvplot-js - the JavaScript / TypeScript plotting packages
- cfbfastR - college football
- hoopR - men’s basketball
- wehoop - women’s basketball
- baseballr - baseball
- fastRhockey - hockey
- oddsapiR - betting odds
- sportyR - playing surfaces
- sportsdataverse-py - the Python package
- sportsdataverse-R - the R meta-package



