rguides

Advanced Table Formatting with gt

Quick start: make a table in 5 minutes

The gt package turns data frames into publication-quality tables with a concise grammar of table formatting. You start with gt(), add structure with headers and spanners, apply formatting functions to individual columns, and style cells based on data values. The code below creates a table in a single pipeline: pipe data into gt(), chain formatting, and display. The rowname_col argument designates a column as row labels, while fmt_number() and fmt_currency() control how numeric values render.

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. The recipes that follow show how to layer on more sophisticated table formatting: conditional colors, spanner headers, row groups, and custom themes.

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 object
  • tab_header(), adds title and subtitle
  • fmt_currency(), formats price columns as USD currency
  • fmt_date(), formats the date column
  • fmt_number(suffixing = TRUE), turns volume like 131000000 into 131M
  • cols_align(align = "center"), centers all columns except date
  • cols_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:

  1. cols_add(color = paste0(hex)) creates a helper column by copying the hex values, paste0() handles the type correctly without needing rep()
  2. from_column(column = "hex") reads each hex value and applies it as the fill color for the corresponding cell in the color column
  3. cols_hide(columns = hex) hides the original hex column from display
  4. Using font = "Courier New" avoids the system_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)) before pivot_wider() ensures year columns are character type, so where(is.numeric) targets only the population columns
  • tab_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 colors
  • tab_source_note() adds attribution at the bottom

This recipe demonstrates the structure-first workflow that gt encourages: build the table skeleton with gt() and pivot_wider(), apply numeric formatting across all columns with where(is.numeric), then layer on visual styling. The dark header and alternating stripes follow conventions from financial reporting, making the output feel familiar to readers accustomed to professional publications.

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:

  1. tab_options(), global table appearance (backgrounds, borders, padding)
  2. opt_row_striping(), alternating row colors
  3. tab_footnote() + opt_footnote_marks(), footnote with letter-style marks
  4. tab_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.

Spanners and column groups

gt supports column spanners, headers that span multiple columns, via tab_spanner(). Pass the spanner label and the columns it covers. Nested spanners (a spanner above another spanner) are also supported. This is useful for tables with repeated column groups like “Before” and “After” each having “Mean” and “SD” sub-columns.

Inline data visualizations

gt_sparkline() from the gtExtras package embeds miniature line charts in table cells. gt_color_rows() and gt_data_color() apply color scales to numeric columns, turning a table into a heat map. gt_plt_bar() renders horizontal bar charts in cells. These visualizations communicate magnitude at a glance without requiring a separate figure.

Exporting tables

gtsave("table.html") exports to a standalone HTML file. gtsave("table.png") renders to PNG via webshot2, which requires a headless browser. gtsave("table.docx") or gtsave("table.rtf") exports to Word or RTF for use in documents. For PDF output, embed the gt table in a Quarto or R Markdown document with pdf_document output, gt renders correctly in both LaTeX and HTML-based PDF workflows.

Row groups and stubs

gt_group() wraps multiple gt tables with shared headers, useful for multi-panel tables with different row sets sharing column definitions. row_group_order() sets the display order of row groups. grand_summary_rows() adds a footer row summing all groups, separate from group-level summary_rows(). The stub column (first column used as row labels) is activated with gt(rowname_col = "name") and styled independently from body columns.

Spanning column headers

tab_spanner(gt_tbl, label = "Group Label", columns = c(col1, col2, col3)) adds a spanning header above multiple columns. Multiple spanners can be stacked. tab_spanner_delim(delim = "_") automatically creates spanners from column names that share a prefix before the delimiter: columns "score_2022", "score_2023", "score_2024" become three columns under a “score” spanner.

cols_label(.list = list(col1 = "Display Name 1", col2 = "Display Name 2")) relabels multiple columns at once. Column labels support markdown via md() and HTML via html(): cols_label(pval = md("*p*-value")) italicizes “p”.

Summary rows and grand totals

summary_rows(gt_tbl, columns = c(q1, q2, q3), fns = list(Total = ~ sum(.))) adds a summary row below the data, computing the sum of each specified column. Multiple functions: fns = list(Mean = ~ mean(.), Total = ~ sum(.)) adds two summary rows.

grand_summary_rows(gt_tbl, columns = numeric_cols, fns = list(Total = ~ sum(.))) adds a grand total row that spans all row groups, appearing at the bottom of the table.

tab_row_group(gt_tbl, group = "Group Name", rows = condition) divides the table into named row groups. row_group_order(groups = c("Group A", "Group B", "Other")) sets the display order.

Footnotes and source notes

tab_footnote(gt_tbl, footnote = "Note text", locations = cells_column_labels(columns = col1)) adds a footnote to a column label. locations = cells_body(columns = col1, rows = 3) adds a footnote to a specific cell. Footnotes are numbered automatically.

tab_source_note(gt_tbl, source_note = md("Source: [Data.gov](https://data.gov)")) adds a source citation below the table. Multiple calls stack source notes.

Exporting and rendering

gtsave(gt_tbl, "table.html") saves as HTML. gtsave(gt_tbl, "table.png") saves as a PNG image (requires the webshot2 package and a Chrome/Chromium installation). gtsave(gt_tbl, "table.pdf") saves as PDF.

as_raw_html(gt_tbl) returns the HTML as a string, suitable for embedding in emails with blastula. as_latex(gt_tbl) returns LaTeX code for PDF reports. gt_tbl %>% as_gt_tbl() returns the underlying data for programmatic inspection.

In Quarto and R Markdown HTML output, simply return a gt object from a code chunk and it renders automatically. In PDF output, as_latex(gt_tbl) or gt_tbl %>% knitr::knit_print() handles rendering.

Beyond basic formatting

The gt package’s basic table creates readable output from a data frame with minimal code. Advanced gt features address the needs of publication-quality tables: spanning column headers that group related columns, footnotes that explain data sources or methodological choices, conditional cell formatting that highlights outliers, and summary rows that aggregate data with the same visual treatment as regular rows.

These features compose. A table can have spanning headers and conditional formatting and footnotes simultaneously. gt applies formatting in layers, and each layer adds to the previous without interfering. Understanding the order in which gt processes a table, first structure, then format, then style, then render, helps predict how combinations of features will behave.

Conditional formatting

Conditional formatting draws attention to values that meet specific criteria, cells above a threshold, outliers, or the maximum in each row. The data_color function maps a continuous or discrete scale to background colors, turning a table column into a heatmap. The tab_style function applies arbitrary CSS styling to cells selected by a location specifier and a condition.

For highlighting specific cells, the cells_body location with a condition argument selects cells matching a logical expression. The style applied can be text color, background color, font weight, or any CSS property that gt supports. Combining multiple tab_style calls lets you apply different styles for different conditions, one style for values above the upper threshold and another for values below the lower threshold.

Exporting and integration

Tables created with gt render to HTML by default. For publication PDFs, call gtsave with a .pdf extension, which converts the table through a headless browser. For Word documents, saving as .docx produces a Word-compatible table. For including gt tables in R Markdown or Quarto documents, the print method automatically produces the appropriate output format for the current rendering context, HTML in HTML documents, LaTeX in PDF documents.

Summary rows and aggregation

Grand summary rows and group summary rows add aggregated rows at the bottom of the table or at the bottom of each group. The summary_rows function takes column selectors and a list of aggregation functions. Each function in the list becomes one summary row. You can add multiple summary rows — a sum row and an average row — by providing multiple functions.

Summary rows inherit the column formatters applied to regular rows by default. If a column is formatted as currency, the summary row for that column also formats as currency. This consistency reduces the common mistake of showing a raw number in a summary row when the body cells are formatted. Override the formatting per summary row by providing explicit format functions in the summary_rows call.

See also