Advanced Table Formatting with gt
Quick Start: Make a Table in 5 Minutes
library(gt)
exibble |>
gt(rowname_col = "row") |>
tab_header(title = "Quick Start Table") |>
fmt_number(columns = num, decimals = 2) |>
fmt_currency(columns = currency, currency = "USD")
That’s it. Pipe data into gt(), chain formatting, print. Read on for the details.
Recipe 1: Formatted Financial Table
Build a table showing stock prices with currency, dates, and large number formatting.
library(gt)
library(dplyr)
sp500_sample <- sp500 |>
filter(date >= as.Date("2010-06-07") & date <= as.Date("2010-06-14")) |>
select(-adj_close)
sp500_sample |>
gt() |>
tab_header(
title = "S&P 500",
subtitle = "June 7–14, 2010"
) |>
fmt_currency(columns = c(open, close, high, low)) |>
fmt_date(columns = date, date_style = "wd_m_day_year") |>
fmt_number(columns = volume, suffixing = TRUE) |>
cols_align(align = "center", columns = -date) |>
cols_width(
date ~ px(140),
everything() ~ px(100)
)
What each line does:
gt()— creates the table objecttab_header()— adds title and subtitlefmt_currency()— formats price columns as USD currencyfmt_date()— formats the date columnfmt_number(suffixing = TRUE)— turns volume like131000000into131Mcols_align(align = "center")— centers all columns except datecols_width()— sets column widths in pixels
Recipe 2: Conditional Cell Styling
Highlight cells that meet a condition — for example, mark high values in green and low values in red.
library(gt)
countrypops |>
filter(country_code_3 %in% c("CHN", "IND", "USA", "GBR")) |>
filter(year == 2020) |>
gt(rowname_col = "country_code_3") |>
# Green for >200M population, red for <100M
fmt_number(columns = population, suffixing = TRUE, n_sigfig = 3) |>
tab_style(
style = cell_fill(color = "#d4edda"),
locations = cells_body(
columns = population,
rows = population > 200000000
)
) |>
tab_style(
style = cell_fill(color = "#f8d7da"),
locations = cells_body(
columns = population,
rows = population < 100000000
)
) |>
tab_style(
style = cell_text(weight = "bold"),
locations = cells_body(columns = population)
) |>
cols_align(align = "center") |>
opt_row_striping()
How the conditional logic works:
cells_body(columns = population, rows = population > 200000000)targets only rows where population exceeds 200 million- Each
tab_style()call adds a new layer of styling — later calls can override earlier ones for the same cells opt_row_striping()adds alternating row shading on top of the conditional fills
Recipe 3: Color Cells from Data Values
Use hex color codes stored in your data to color cells dynamically with from_column().
library(gt)
color_table <- tibble(
fruit = c("Apple", "Banana", "Cherry"),
hex = c("#FF5733", "#28B463", "#E74C3C"),
calories = c(52, 89, 50)
)
color_table |>
gt(rowname_col = "fruit") |>
# Initialize color column with empty strings — no sub_missing needed
cols_add(color = paste0(hex)) |>
tab_style(
style = cell_fill(color = from_column(column = "hex")),
locations = cells_body(columns = color)
) |>
tab_style(
style = cell_text(font = "Courier New"),
locations = cells_body()
) |>
cols_width(stub() ~ px(100), everything() ~ px(120)) |>
cols_hide(columns = hex) |>
tab_options(table_body.hlines.style = "none")
Key steps:
cols_add(color = paste0(hex))creates a helper column by copying the hex values —paste0()handles the type correctly without needingrep()from_column(column = "hex")reads each hex value and applies it as the fill color for the corresponding cell in thecolorcolumncols_hide(columns = hex)hides the original hex column from display- Using
font = "Courier New"avoids thesystem_fonts()dependency from the systemfonts package
Recipe 4: Multi-Level Headers with Spanners and Row Groups
Create a publication-ready table with row groups and multi-column spanners, like you see in financial reports.
library(gt)
library(dplyr)
countrypops |>
filter(country_code_3 %in% c("CHN", "IND", "USA", "GBR")) |>
filter(year >= 2015) |>
# Convert year to character so it's not treated as numeric
mutate(year = as.character(year)) |>
pivot_wider(names_from = year, values_from = population) |>
gt(rowname_col = "country_code_3") |>
tab_spanner(
label = "Population (millions)",
columns = where(is.numeric)
) |>
tab_header(
title = "Country Population Over Time",
subtitle = "Selected countries, 2015–2020"
) |>
fmt_number(columns = where(is.numeric), decimals = 0, use_seps = TRUE) |>
cols_align(align = "center", columns = where(is.numeric)) |>
cols_width(stub() ~ px(120), everything() ~ px(110)) |>
tab_style(
style = cell_fill(color = "#2c3e50"),
locations = cells_title()
) |>
tab_style(
style = cell_text(color = "white", weight = "bold"),
locations = cells_column_spanners()
) |>
opt_row_striping(row_striping_weight = 1) |>
tab_source_note(source_note = "Source: countrypops dataset (gt package)")
What makes this look polished:
mutate(year = as.character(year))beforepivot_wider()ensures year columns are character type, sowhere(is.numeric)targets only the population columnstab_spanner()creates a multi-column header grouping all year columns- Dark header with white text via
cell_fill()+cell_text() opt_row_striping()adds alternating row colorstab_source_note()adds attribution at the bottom
Recipe 5: Styled Table with Tab Options
Take full control of the table’s look with tab_options() and the opt_*() family.
library(gt)
exibble |>
gt(rowname_col = "row") |>
tab_header(
title = "Styled Table Example",
subtitle = "Demonstrating tab_options and opt_* functions"
) |>
fmt_number(columns = num, decimals = 2) |>
fmt_currency(columns = currency) |>
fmt_date(columns = date) |>
tab_options(
table.background.color = "#f8f9fa",
table.font.size = "small",
column_labels.background.color = "#2c3e50",
column_labels.font.color = "white",
column_labels.font.weight = "bold",
row_group.background.color = "#ecf0f1",
data_row.padding = px(4),
table_body.hlines.style = "solid",
table_body.hlines.color = "#dee2e6",
table_body.vlines.style = "none",
table.outline.color = "#2c3e50",
table.outline.width = px(2)
) |>
opt_row_striping(row_striping_weight = 1) |>
tab_footnote(
footnote = "Sample footnote",
locations = cells_column_labels(columns = num)
) |>
opt_footnote_marks(marks = "letters") |>
tab_source_note(source_note = "Source: exibble dataset (gt package)")
The styling layers:
tab_options()— global table appearance (backgrounds, borders, padding)opt_row_striping()— alternating row colorstab_footnote()+opt_footnote_marks()— footnote with letter-style markstab_source_note()— attribution at the bottom
Recipe 6: Using dplyr Groups for Row Groups
If your data is already grouped with dplyr::group_by(), gt picks it up automatically.
library(gt)
library(dplyr)
mtcars |>
mutate(model = rownames(mtcars)) |>
select(model, mpg, cyl, disp, hp) |>
group_by(cyl) |>
gt(rowname_col = "model") |>
tab_header(title = "Cars by Cylinder Count") |>
fmt_number(columns = c(mpg, disp, hp), decimals = 1) |>
opt_row_striping() |>
cols_align(align = "center", columns = -model) |>
cols_width(stub() ~ px(140), everything() ~ px(80)) |>
tab_options(row_group.background.color = "#e8f4f8")
Important: when data is grouped with dplyr::group_by(), the groupname_col argument in gt() is ignored. Use ungroup() before gt() if you do not want row groups.
Saving Your Table
Export your table with gtsave():
table <- exibble |>
gt(rowname_col = "row") |>
fmt_number(columns = num, decimals = 2)
gtsave(table, "my_table.html") # HTML output
gtsave(table, "my_table.png", zoom = 2) # raster image
gtsave(table, "my_table.tex") # LaTeX output
gtsave(table, "my_table.rtf") # RTF output
The zoom argument for image output controls resolution. Higher values produce sharper output.
See Also
- gt function reference
- tab_style reference
- gtsummary — summary and regression tables
- gtExtras — sparklines, heatmaps, and more