Publication-Ready Tables with gt
Introduction
The gt package (short for “grammar of tables”) is an R package that transforms data frames into publication-ready tables. Developed by the RStudio team, gt provides a consistent API for creating tables that look professional without needing HTML, CSS, or LaTeX expertise. This guide covers everything from basic table creation to advanced styling, exporting, and integrating gt tables into reports and dashboards.
Why use gt? If you’ve ever struggled with formatting tables in R for reports, papers, or dashboards, gt solves that problem. Creating publication-ready tables with gt means you define the data, add formatting layers, and configure the presentation with a grammar that mirrors how ggplot2 handles charts. The output renders as HTML by default, but you can export to PNG, PDF, LaTeX, and RTF.
Install gt from CRAN. The tidyverse is also useful for data manipulation before passing data into gt:
install.packages("gt")
install.packages("tidyverse")
After installation, load both packages with library(). gt follows R’s standard package-loading convention and does not auto-attach any datasets or override existing functions, so loading it alongside the tidyverse is completely safe, since the two packages are designed to coexist without namespace conflicts.
library(gt)
library(tidyverse)
Basic table creation
Creating a basic gt table is straightforward: pass any data frame to gt() and it renders immediately. The function accepts standard data frames, tibbles, and most objects with a tabular structure. Behind the scenes, gt() converts the data into an internal table representation that stores both the raw values and all formatting instructions as separate layers, leaving the source data frame completely untouched.
# Sample data
sales_data <- tibble(
product = c("Widget A", "Widget B", "Widget C", "Widget D"),
quarter = c("Q1", "Q1", "Q1", "Q1"),
units_sold = c(150, 230, 180, 95),
revenue = c(4500, 6900, 5400, 2850)
# Create the table
gt_table <- gt(sales_data)
gt_table
The output displays a clean table with column names as headers. Column labels inherit their names from the data frame columns by default, which often means lowercase_underscore_names that look unpolished in a finished table. The cols_label() function lets you rename them to human-readable labels, control their order, and even hide columns you don’t want displayed. Each renaming maps a raw column name to a display label, keeping the underlying data unchanged.
You can customize column labels using cols_label():
gt(sales_data) %>%
cols_label(
product = "Product",
quarter = "Quarter",
units_sold = "Units Sold",
revenue = "Revenue ($)"
)
Add a title and subtitle for context. Titles appear in a large font above the column labels and serve as the primary identifier for the table. Subtitles sit just below the title in a smaller font and can include methodology notes, date ranges, or units. In multi-table reports, these fields help readers understand what each table contains without scanning the full caption or surrounding text:
gt(sales_data) %>%
tab_header(
title = "Q1 Sales Report",
subtitle = "Product performance by unit volume and revenue"
)
Formatting columns
gt provides specialized formatting functions that change how numbers appear in the rendered table without modifying the underlying data frame. This clean separation means you can format for display while keeping the raw values available for computation.
Currency formatting
fmt_currency() formats numeric columns as monetary values with currency symbols and appropriate decimal precision. The currency argument accepts ISO 4217 codes (USD, EUR, GBP) and positions the symbol according to locale conventions. For financial statements, accounting = TRUE displays negative values in parentheses rather than with a minus sign, matching accounting conventions used in income statements and balance sheets.
gt(sales_data) %>%
fmt_currency(columns = revenue, currency = "USD")
# Accounting-style negative values in parentheses
financial_data <- tibble(
category = c("Revenue", "Expenses", "Net Income"),
amount = c(150000, -95000, 55000)
)
gt(financial_data) %>%
fmt_currency(amount, currency = "USD", accounting = TRUE)
Percentage formatting
fmt_percent() multiplies values by 100 and appends a percent sign, so a value of 0.245 displays as 24.5%. The decimals argument controls the number of decimal places shown. This is essential for displaying rates, proportions, and growth metrics in a human-readable form, especially in business reports where percentages are more intuitive than raw decimal values.
growth_data <- tibble(
metric = c("User Growth", "Retention", "Conversion"),
value = c(0.245, 0.78, 0.032)
)
gt(growth_data) %>%
fmt_percent(value, decimals = 1)
Date and time formatting
fmt_date() and fmt_datetime() apply locale-aware date formatting using style presets. The date_style argument accepts named styles like "wd_m_day_year" (Wednesday, March 15, 2024) or numeric codes, giving you control over how dates appear without manual string conversion. For international reports, the locale can be adjusted so dates follow the conventions of the target audience rather than the system default.
event_data <- tibble(
event = c("Conference", "Workshop", "Meetup"),
date = as.Date(c("2024-03-15", "2024-04-22", "2024-05-10"))
)
gt(event_data) %>%
fmt_date(date, date_style = "wd_m_day_year")
Number formatting
fmt_number() controls decimal places, thousands separators, and significant figures. Use decimals = 0 for integer counts, decimals = 2 for precise financial figures, and the sep_mark argument (defaulting to ",") to make large numbers scannable. Unlike fmt_currency(), this function does not add a currency symbol, making it appropriate for counts, ratios, and other non-monetary quantities.
gt(sales_data) %>%
fmt_number(units_sold, decimals = 0) %>%
fmt_number(revenue, decimals = 2, sep = ",")
Styling
Themes
gt includes built-in themes via opt_table_theme(). Themes apply coordinated sets of spacing, border, and color styles in one call, saving you from manually configuring each visual element. The “spa” theme produces a clean, bordered look suitable for general use, but other themes change the whole visual character of the table.
gt(sales_data) %>%
opt_table_theme(theme = "spa")
Other available themes include “default”, “slate”, “dark”, and “honda”. When presenting tables alongside branded reports, choosing a theme that matches your existing color palette produces a cohesive look without writing custom CSS.
Cell colors
Apply background colors to cells using data_color(), which maps numeric column values onto a continuous color palette. The scales::col_numeric() function defines the mapping from data values to color codes, so low values get light shades and high values get dark shades. This technique replaces the need for separate conditional styling rules when the goal is to show magnitude visually.
gt(sales_data) %>%
data_color(
columns = revenue,
colors = scales::col_numeric(
palette = c("white", "green"),
domain = c(0, max(sales_data$revenue))
)
)
Borders and alignment
Control border styling with tab_style(), which applies CSS-like style declarations to specific cell locations. cells_column_labels() targets only the header row, so the bold and border styles affect column names without touching data cells. This separation of targeting and styling means you can apply borders to the header, specific columns, or individual cells independently, building up the desired appearance in layers.
gt(sales_data) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_column_labels()
) %>%
tab_style(
style = cell_borders(sides = "bottom", color = "black", weight = px(2)),
locations = cells_column_labels()
)
Align text with opt_align_table_header() and cols_align(). The header alignment sets the position of the title and subtitle relative to the table width, while cols_align() controls individual column alignment at the data cell level. Numeric columns are conventionally right-aligned, text columns left-aligned, and grouping or category columns often look best centered. Using everything() as the column selector applies the alignment to all columns at once:
gt(sales_data) %>%
opt_align_table_header(align = "center") %>%
cols_align(align = "center", columns = quarter)
Advanced features
Row groups
Group rows by a column using row_group_order(). Row groups divide the table into labeled sections, making it easier to scan by category. The groups argument specifies the order in which groups appear, which determines how rows are sorted and labeled in the output; if your data has a natural hierarchy (regions, departments, time periods), row groups render that structure visually.
regional_sales <- tibble(
region = c("North", "North", "South", "South", "East", "East"),
product = c("Widget A", "Widget B", "Widget A", "Widget B", "Widget A", "Widget B"),
sales = c(500, 750, 620, 480, 890, 670)
gt(regional_sales) %>%
row_group_order(groups = c("North", "South", "East"))
Column spanning
Create spanning headers with tab_spanner(), which groups multiple columns under a shared label that sits above the individual column names. This is how you create multi-level column headers like “Q1” spanning both sales and units columns. Spanning headers are especially useful when columns share a common category prefix, like time periods, demographic groups, or experimental conditions. Each tab_spanner() call adds another layer to the column header hierarchy:
combined_data <- tibble(
product = c("Widget A", "Widget B"),
q1_sales = c(150, 230),
q2_sales = c(180, 265),
q1_units = c(4500, 6900),
q2_units = c(5400, 7950)
gt(combined_data) %>%
tab_spanner(label = "Q1", columns = c(q1_sales, q1_units)) %>%
tab_spanner(label = "Q2", columns = c(q2_sales, q2_units))
Footnotes
Add footnotes with tab_footnote(). Each footnote links to a specific location in the table — a column label, a body cell, a summary row, or a row group label — and displays as a numbered marker with the footnote text in the table footer. Footnotes are the standard way to note data sources, explain adjustments, or qualify entries without cluttering the table body.
gt(sales_data) %>%
tab_footnote(
footnote = "Data includes returns",
locations = cells_column_labels(columns = units_sold)
)
Export options
gt tables can be saved to multiple output formats using gtsave(). The same table object can produce HTML for web reports, PNG for slide decks, LaTeX for academic manuscripts, and PDF for print. This single-source, multiple-output approach means you prepare the table once and export it to whatever format each deliverable requires.
gt_table <- gt(sales_data)
gtsave(gt_table, "sales_table.html")
PNG export uses the webshot2 package to capture a screenshot of the rendered HTML table, so the output preserves all formatting exactly as it appears in the RStudio viewer or browser. This is the recommended format for presentations and Word documents.
gtsave(gt_table, "sales_table.png")
LaTeX output integrates directly with academic paper workflows. The generated .tex file can be included in a LaTeX document with \input{} and respects the surrounding document’s font, page width, and styling. This makes it the preferred format for journal submissions, theses, and any publication venue that requires LaTeX source files rather than embedded images.
gtsave(gt_table, "sales_table.tex")
PDF export combines LaTeX generation with a LaTeX compiler to produce a standalone PDF. This is the simplest option when you need a print-ready table without embedding it in a larger document. The resulting PDF can be shared directly, attached to emails, or uploaded to preprint servers without the recipient needing any R or LaTeX toolchain.
gtsave(gt_table, "sales_table.pdf")
Conditional styling
tab_style() applies CSS styles to cells matching a condition. cells_body(columns = revenue, rows = revenue < 0) targets negative revenue cells; pair with cell_fill(color = "red") and cell_text(weight = "bold") to highlight them. data_color() applies a continuous color scale to a numeric column. cols_merge_uncert() merges a value column and its uncertainty column into a single “value ± uncertainty” display.
Summary rows
summary_rows() adds aggregate rows to row groups. grand_summary_rows() adds a summary row at the bottom of the entire table. The fns argument accepts a named list of functions: fns = list(Total = sum, Mean = mean) creates two summary rows per group. Use fmt_*() on summary rows by targeting cells_summary() in a tab_style() or fmt_*() call.
Source notes and footnotes
tab_source_note() adds a note below the table for data attribution or methodology notes. tab_footnote() adds numbered footnotes linked to specific cells, columns, or rows. The locations argument specifies where the footnote marker appears: cells_body(columns = revenue, rows = revenue > 1000000) marks high-revenue cells. tab_options(footnotes.marks = "numbers") uses numbers instead of symbols for footnote markers.
Table structure and parts
A gt table has eight parts: the header (title and subtitle), column spanners, column labels, the stub (optional row labels), the body (data cells), row group labels, summary rows, and the footer (source notes and footnotes). Each part is controlled by distinct functions.
gt(df, rowname_col = "name") designates a column as the stub (row names). The stub gets special formatting and is always pinned to the left. Without a stub, all columns are treated uniformly.
tab_header(title = "My Table", subtitle = "Source data") adds a title. md() enables markdown formatting: tab_header(title = md("**Revenue by Region**")). html() enables HTML: tab_header(title = html("<span style='color: navy;'>Revenue</span>")).
Number formatting
fmt_number(columns = revenue, decimals = 2, use_seps = TRUE) formats numbers with two decimal places and thousands separators. fmt_currency(columns = revenue, currency = "USD") adds a currency symbol. fmt_percent(columns = rate, decimals = 1) formats as percentage. fmt_scientific() uses scientific notation.
fmt_date(columns = date, date_style = 5) formats dates, date_style selects from about 14 built-in formats. fmt_time() and fmt_datetime() handle time and datetime columns. fmt_missing(columns = everything(), missing_text = "—") replaces NA values with a dash.
fmt(columns = value, fns = function(x) paste0(round(x/1000, 1), "K")) applies a custom formatting function. This handles any display format not covered by built-in fmt_* functions.
Styling specific cells
tab_style(style = cell_fill(color = "lightblue"), locations = cells_body(columns = status, rows = status == "active")) applies a fill to cells matching a condition. Styles stack: multiple tab_style() calls apply in order.
cell_text(weight = "bold", color = "darkred", size = px(14)) sets text properties. cell_fill(color = "#f0f0f0", alpha = 0.8) sets background. cell_borders(sides = "bottom", color = "black", weight = px(2)) draws cell borders.
data_color(columns = score, fn = scales::col_numeric("Blues", c(0, 100))) applies a continuous color scale based on values. data_color(columns = category, fn = scales::col_factor(c("red", "blue", "green"), levels = c("low", "medium", "high"))) applies categorical coloring.
Spanner labels and column configuration
tab_spanner(label = "First Half", columns = c(jan, feb, mar, apr, may, jun)) groups columns under a shared label. Nested spanners are possible. tab_spanner(label = md("**2024**"), columns = matches("2024")) selects columns by regex pattern.
cols_align(align = "right", columns = where(is.numeric)) right-aligns all numeric columns. cols_width(name ~ px(150), everything() ~ px(80)) sets column widths. cols_move(columns = total, after = last_col()) reorders columns.
Output and integration
as_raw_html(tbl) returns the HTML string for embedding in emails or web pages. gtsave(tbl, "table.png") saves as an image (requires webshot2). In Quarto and R Markdown, simply return the gt object from a chunk.
gt_preview(tbl) shows the first and last rows — useful for inspecting large tables without rendering all rows. This is the gt equivalent of head() and tail() for quick inspection during development.
Design philosophy of gt
The gt package follows a grammar approach to tables similar to how ggplot2 applies a grammar of graphics to charts. You define the data, then add formatting layers, then configure the presentation. This layered approach makes it easy to start simple and add complexity incrementally.
A well-formatted table communicates data at a glance. The same design principles that apply to charts apply to tables: remove decorative elements that don’t carry information, use formatting to draw attention to important values, ensure color and typography have meaning rather than just aesthetic function.
Publication-quality tables for academic papers have specific conventions: variable names as column headers, statistics with consistent decimal places, significance indicators as superscripts or footnotes, standard deviations in parentheses below means. The gt package supports all of these patterns, making it suitable for producing table-ready outputs for journals and reports.
For business dashboards, tables serve a different purpose: showing current values for multiple metrics, often with conditional formatting to flag off-target values. The reactable package is often better for interactive business tables, while gt excels at static formatted outputs for reports and publications.
The choice between gt, kableExtra, and reactable depends on the use case. gt for polished static tables that will be exported to multiple formats. kableExtra for lightweight R Markdown integration where full gt capability is not needed. reactable for interactive tables in Shiny or HTML reports where users need to sort, filter, or interact with the data.
Summary statistics integration
gt pairs well with gtsummary for publication-ready statistical tables. gtsummary::tbl_summary() creates a Table 1 (baseline characteristics table) from a data frame. as_gt() converts it to a gt object for further customization. gtsummary::tbl_regression() formats regression model output. These tools produce journal-ready tables without manual formatting, following conventions for medical and epidemiological literature automatically.
Exporting tables
gt tables render in HTML by default, which works directly in R Markdown and Quarto documents. For other formats, gtsave(tbl, "table.png") exports a PNG screenshot using the webshot2 package. gtsave(tbl, "table.pdf") exports PDF. as_latex(tbl) returns the LaTeX source for inclusion in academic manuscripts. The HTML output can also be pasted into word processors that accept HTML, preserving the formatting. This flexibility makes gt suitable for generating tables once and deploying them across multiple output formats without reformatting.
See also
- Data Visualization with ggplot2 — for creating charts and graphs to accompany your tables
- Data Manipulation with dplyr — for preparing data before feeding it into gt
- Interactive Tables with reactable — for sortable, filterable tables in Shiny apps
- Quarto Analysis Reports — embedding gt tables in reproducible reports
- Building R Packages — package development best practices