The visual appearance of a gt table has a tremendous impact on how effectively it communicates its data. While a table may contain valuable information, poor aesthetic choices can obscure important patterns or make comparisons difficult. Conversely, styling decisions such as selective use of color, appropriate typography, judicious borders, and effective spacing can transform raw data into a compelling visual narrative.
This chapter explores the rich set of tools gt provides for controlling the aesthetic dimensions of tables. We begin with data-driven coloring, where cell colors communicate quantitative relationships. We then examine precision styling through tab_style(), which permits targeted modifications to any table location. The tab_options() function opens up a vast landscape of global table settings, while various opt_*() convenience functions provide quick access to commonly desired configurations. Finally, we explore interactive HTML tables, which offer readers additional ways to explore and understand tabular data.
These aesthetic controls exist not merely for decoration but to enhance comprehension. Color can highlight extremes or encode continuous variables. Typography establishes hierarchy and improves readability. Borders delineate structure. Padding affects density and scannability. Understanding how these elements work together (and knowing when restraint serves better than embellishment) is essential for creating truly effective display tables.
8.1 Coloring data according to their values
Color represents one of the most powerful visual channels available for encoding data. When applied thoughtfully to table cells, background colors can reveal patterns, highlight outliers, and provide an immediate sense of magnitude that numbers alone cannot convey. The human visual system is good at perceiving color gradients and categorical distinctions, making data coloring an effective technique for enhancing table comprehension.
The data_color() function in gt provides sophisticated mechanisms for mapping data values to colors. It supports multiple coloring methods (numeric interpolation, binning, quantiles, and categorical factors) and offers extensive palette options ranging from built-in R palettes to the rich collections available through viridis and RColorBrewer packages, as well as the vast selection accessible via the paletteer package. The function also handles practical concerns like automatic text recoloring for accessibility and flexible targeting of specific cells.
8.1.1data_color()
The data_color() function performs cell colorization based on the data values themselves. This is fundamentally different from static styling; the colors emerge from and communicate the underlying data.
The simplest invocation colors the entire table using default settings:
exibble|>gt()|>data_color()
num
char
fctr
date
time
datetime
currency
row
group
1.111e-01
apricot
one
2015-01-15
13:35
2018-01-01 02:22
49.950
row_1
grp_a
2.222e+00
banana
two
2015-02-15
14:40
2018-02-02 14:33
17.950
row_2
grp_a
3.333e+01
coconut
three
2015-03-15
15:45
2018-03-03 03:44
1.390
row_3
grp_a
4.444e+02
durian
four
2015-04-15
16:50
2018-04-04 15:55
65100.000
row_4
grp_a
5.550e+03
NA
five
2015-05-15
17:55
2018-05-05 04:00
1325.810
row_5
grp_b
NA
fig
six
2015-06-15
NA
2018-06-06 16:11
13.255
row_6
grp_b
7.770e+05
grapefruit
seven
NA
19:10
2018-07-07 05:22
NA
row_7
grp_b
8.880e+06
honeydew
eight
2015-08-15
20:20
NA
0.440
row_8
grp_b
This basic example applies the default R color palette to all columns. Numeric columns receive continuous color interpolation while character and factor columns use categorical coloring. Notice how the text color automatically adjusts for contrast where dark text appears on light backgrounds and vice versa.
For more targeted and meaningful coloring, we typically specify the columns, method, and palette:
Warning: Some values were outside the color scale and will be treated as NA
date
open
high
low
close
2015-01-15
$2,013.75
$2,021.35
$1,991.47
$1,992.67
2015-01-14
$2,018.40
$2,018.40
$1,988.44
$2,011.27
2015-01-13
$2,031.58
$2,056.93
$2,008.25
$2,023.03
2015-01-12
$2,046.13
$2,049.30
$2,022.58
$2,028.26
2015-01-09
$2,063.45
$2,064.43
$2,038.33
$2,044.81
2015-01-08
$2,030.61
$2,064.08
$2,030.61
$2,062.14
2015-01-07
$2,005.55
$2,029.61
$2,005.55
$2,025.90
2015-01-06
$2,022.15
$2,030.25
$1,992.44
$2,002.61
2015-01-05
$2,054.44
$2,054.44
$2,017.34
$2,020.58
2015-01-02
$2,058.90
$2,072.36
$2,046.04
$2,058.20
In this table of S&P 500 data, we apply a red-yellow-green palette to the price columns. The domain argument explicitly sets the value range for color mapping, ensuring consistent coloring across all four columns. Red indicates lower prices while green indicates higher values. This color scheme immediately reveals the relative position of each value within the specified range.
The method argument determines how values map to colors. The "numeric" method provides linear interpolation, ideal for continuous data. The "bin" method groups values into discrete categories:
Here, the U.S. population values are grouped into five bins, each receiving a distinct shade of blue. This method is useful when you want to emphasize categorical differences rather than continuous gradation. The boundaries are determined automatically based on the data range.
The "quantile" method ensures equal numbers of observations in each color category, which can reveal distributional patterns that "bin" might obscure:
With quartile coloring, each of the four colors appears in roughly equal frequency, regardless of how the actual values are distributed. This is particularly valuable for skewed distributions where numeric or bin methods might concentrate most observations in a single color.
For categorical data, the "factor" method maps distinct values to distinct colors:
This example demonstrates explicit color assignment using a named vector. Each drivetrain type receives its specified color, providing complete control over the categorical mapping.
The target_columns argument enables indirect coloring, where one column’s values determine another column’s colors:
The population values drive the coloring, but the colors appear in the separate color_indicator column. This technique creates a visual “legend” column that displays the color scale while keeping the numeric values unobscured.
Row-wise coloring analyzes values across rows rather than down columns:
With direction = "row", each row’s color scale is computed independently. This reveals patterns within rows (how solar zenith angles vary through the day for each month) rather than comparing the same time across months.
For maximum control, you can supply a custom color-mapping function via the fn argument:
Using scales::col_numeric() directly provides access to additional options not exposed through data_color()’s simplified interface. You can also write entirely custom functions for specialized coloring logic.
8.2 Adding style to various locations
Beyond data-driven coloring, tables often require targeted stylistic modifications to emphasize particular elements, establish visual hierarchy, or simply improve readability. gt provides a comprehensive styling system built around the tab_style() function and its companion helper functions. This system offers precise control over text properties, background fills, and borders at any table location.
The styling approach in gt separates concerns into three components: what styles to apply (the cell_*() helpers), where to apply them (the cells_*() location helpers), and how to combine them (tab_style() itself). This separation provides both flexibility and clarity, allowing complex styling specifications to remain readable and maintainable.
8.2.1tab_style() and the cell_*() style helpers
The tab_style() function serves as the primary mechanism for applying custom styles to gt tables. It takes two main arguments: style (what styling to apply) and locations (where to apply it).
Function Signature
tab_style(data, style, locations)
Three helper functions define the possible style modifications:
This simple example applies bold navy text to numeric values exceeding 1000. The cells_body() helper targets specific cells using column and row specifications. Rows can be specified by index, name, or logical expression.
Multiple style properties combine naturally within cell_text():
Here we simultaneously modify font size, apply italics, and transform text to uppercase. The combination creates a distinct visual treatment for the character column.
When you need multiple style types (text, fill, borders), combine them in a list:
This stock market example highlights “up” days (close > open) in green and “down” days in pink. Each tab_style() call combines a fill color with bold text, and the row expression determines which rows receive each treatment.
The dark title bar with light text creates strong visual hierarchy, while the subtle gray column labels establish a secondary level.
The complete set of location helpers includes:
cells_title(): table title and subtitle
cells_stubhead(): the stubhead cell
cells_column_spanners(): column spanner labels
cells_column_labels(): individual column labels
cells_row_groups(): row group labels
cells_stub(): stub (row label) cells
cells_body(): main table body cells
cells_summary(): group summary row cells
cells_grand_summary(): grand summary row cells
cells_footnotes(): footnote text
cells_source_notes(): source note text
Each helper accepts arguments appropriate to its context and cells_body() takes columns and rows, while cells_column_labels() takes just columns.
8.2.2tab_style_body()
The tab_style_body() function provides a specialized interface for styling body cells based on their values. While tab_style() requires explicit location specifications, tab_style_body() lets you target cells through value matching, pattern matching, or custom functions.
The values argument searches the entire table body for exact matches, styling any cell containing those precise values. This is particularly useful when highlighting specific reference values or flagging particular codes.
For text pattern matching, use the pattern argument with regular expressions:
Any cell content beginning with "a" receives the specified styling. Regular expressions provide powerful pattern-matching capabilities for text-based targeting.
The fn argument offers maximum flexibility through custom functions:
When targets = "row", finding the value 49.95 causes the entire row to be highlighted, not just that specific cell. This is invaluable for drawing attention to rows meeting specific criteria.
The extents argument controls whether row/column styling extends into the stub:
Including "stub" in extents means the row label also receives the styling, creating visual continuity across the entire row.
8.2.3opt_stylize()
For quick, coordinated table styling without manually specifying colors and borders, opt_stylize() offers six pre-designed style themes in six color variations.
Function Signature
opt_stylize(data, style =1, color ="blue", add_row_striping =TRUE)
exibble|>gt(rowname_col ="row", groupname_col ="group")|>summary_rows( groups ="grp_a", columns =c(num, currency), fns =c("min", "max"))|>tab_header( title ="Example Table with opt_stylize()", subtitle ="Using style 1 with blue color theme")|>opt_stylize(style =1, color ="blue")
Example Table with opt_stylize()
Using style 1 with blue color theme
num
char
fctr
date
time
datetime
currency
grp_a
row_1
1.111e-01
apricot
one
2015-01-15
13:35
2018-01-01 02:22
49.950
row_2
2.222e+00
banana
two
2015-02-15
14:40
2018-02-02 14:33
17.950
row_3
3.333e+01
coconut
three
2015-03-15
15:45
2018-03-03 03:44
1.390
row_4
4.444e+02
durian
four
2015-04-15
16:50
2018-04-04 15:55
65100.000
min
0.1111
—
—
—
—
—
1.39
max
444.4000
—
—
—
—
—
65100.00
grp_b
row_5
5.550e+03
NA
five
2015-05-15
17:55
2018-05-05 04:00
1325.810
row_6
NA
fig
six
2015-06-15
NA
2018-06-06 16:11
13.255
row_7
7.770e+05
grapefruit
seven
NA
19:10
2018-07-07 05:22
NA
row_8
8.880e+06
honeydew
eight
2015-08-15
20:20
NA
0.440
Style 1 with blue coloring provides a clean, professional appearance with subtle header coloring and alternating row stripes.
Let’s compare several style variations:
exibble|>dplyr::select(num, char, currency, date)|>gt()|>tab_header(title ="Style 3, Pink")|>opt_stylize(style =3, color ="pink")
Style 3, Pink
num
char
currency
date
1.111e-01
apricot
49.950
2015-01-15
2.222e+00
banana
17.950
2015-02-15
3.333e+01
coconut
1.390
2015-03-15
4.444e+02
durian
65100.000
2015-04-15
5.550e+03
NA
1325.810
2015-05-15
NA
fig
13.255
2015-06-15
7.770e+05
grapefruit
NA
NA
8.880e+06
honeydew
0.440
2015-08-15
exibble|>dplyr::select(num, char, currency, date)|>gt()|>tab_header(title ="Style 6, Cyan")|>opt_stylize(style =6, color ="cyan")
Style 6, Cyan
num
char
currency
date
1.111e-01
apricot
49.950
2015-01-15
2.222e+00
banana
17.950
2015-02-15
3.333e+01
coconut
1.390
2015-03-15
4.444e+02
durian
65100.000
2015-04-15
5.550e+03
NA
1325.810
2015-05-15
NA
fig
13.255
2015-06-15
7.770e+05
grapefruit
NA
NA
8.880e+06
honeydew
0.440
2015-08-15
The six styles progress from subtle (style 1) to more visually prominent (style 6), with increasing color application to headers, row groups, and summary rows. Available colors are: "blue", "cyan", "pink", "green", "red", and "gray".
The add_row_striping argument controls whether alternating row colors appear:
towny|>dplyr::select(name, population_2021, density_2021)|>dplyr::slice_max(population_2021, n =8)|>gt()|>fmt_integer()|>opt_stylize(style =4, color ="green", add_row_striping =FALSE)
name
population_2021
density_2021
Toronto
2,794,356
4,428
Ottawa
1,017,449
365
Mississauga
717,961
2,453
Brampton
656,480
2,469
Hamilton
569,353
509
London
422,324
1,004
Markham
338,503
1,605
Vaughan
323,103
1,186
Disabling row striping produces a cleaner look for smaller tables where the striping may seem excessive.
8.3 A smorgasbord of table options to choose from
While the styling functions we’ve examined so far provide targeted control over specific elements, gt also offers comprehensive global configuration through tab_options(). This function exposes dozens of parameters governing every aspect of table appearance (from fonts and colors to borders, padding, and even output-format-specific settings).
The scope of tab_options() can feel overwhelming at first, but the parameters follow a consistent naming convention: component.property.subproperty. Understanding this structure makes the function navigable and reveals the systematic nature of gt’s design. Additionally, several opt_*() convenience functions provide shortcuts for commonly used option combinations.
8.3.1tab_options()
The tab_options() function modifies the global settings of a gt table. Its parameter list is extensive, covering table dimensions, fonts, colors, borders, and padding for every table component.
This example sets the table to full width, applies an ivory background, establishes a base font size, and adds a prominent top border. The px() and pct() helper functions ensure proper unit specification.
Heading and column label options control the table’s top regions:
The coordinated blues in the heading and column labels create a cohesive header region. Note how padding adjustments affect the visual weight of these areas.
Body and stub settings control the main data presentation:
This configuration emphasizes row group labels and the stub while lightening the horizontal lines between rows. The reduced padding (data_row.padding = px(4)) creates a much more compact presentation.
For LaTeX output, specific options (latex.*) control document integration:
These settings enable multi-page tables with repeating headers and control float positioning.
8.3.2opt_table_font()
The opt_table_font() function provides a streamlined way to set fonts across the entire table, including support for Google Fonts and system font stacks.
Function Signature
opt_table_font(data, font =NULL, stack =NULL, size =NULL, weight =NULL, style =NULL, color =NULL, add =TRUE)
The font specification includes fallbacks: if IBM Plex Sans isn’t available, the system tries Helvetica, then Arial, then any sans-serif font. This ensures reasonable rendering across different environments.
System font stacks provide reliable cross-platform typography without external dependencies:
The available stacks include: "system-ui", "transitional", "old-style", "humanist", "geometric-humanist", "classical-humanist", "neo-grotesque", "monospace-slab-serif", "monospace-code", "industrial", "rounded-sans", "slab-serif", "antique", "didone", and "handwritten". Each stack contains multiple font families that share similar characteristics and are likely available on most systems.
With both padding dimensions reduced, the table becomes notably denser. This is useful when space is constrained or when displaying many rows where normal padding would create excessive scrolling.
Generous padding improves readability for smaller tables and can make data appear more prestigious or important. The added whitespace gives each value room to breathe.
These functions affect padding at all table locations (heading, column labels, body rows, summary rows, and footer sections) ensuring proportional scaling throughout.
8.3.4opt_row_striping()
Alternating row colors (zebra striping) help readers track across wide tables. The opt_row_striping() function toggles this feature.
Function Signature
opt_row_striping(data, row_striping =TRUE)
towny|>dplyr::select(name, census_div, population_2021, density_2021)|>dplyr::slice_max(population_2021, n =12)|>gt()|>fmt_integer(columns =c(population_2021, density_2021))|>opt_row_striping()
name
census_div
population_2021
density_2021
Toronto
Toronto
2,794,356
4,428
Ottawa
Ottawa
1,017,449
365
Mississauga
Peel
717,961
2,453
Brampton
Peel
656,480
2,469
Hamilton
Hamilton
569,353
509
London
Middlesex
422,324
1,004
Markham
York
338,503
1,605
Vaughan
York
323,103
1,186
Kitchener
Waterloo
256,885
1,878
Windsor
Essex
229,660
1,573
Oakville
Halton
213,759
1,538
Richmond Hill
York
202,022
2,004
The subtle gray stripes make it easier to follow each row from name to density, particularly valuable as tables grow wider.
The stripe color can be customized via tab_options():
The light blue striping with stub inclusion creates a cohesive visual treatment across the entire row.
8.3.5opt_all_caps()
Small-caps and all-caps text can create visual hierarchy and a more formal appearance. The opt_all_caps() function applies uppercase transformation to labels.
exibble|>gt(rowname_col ="row", groupname_col ="group")|>tab_header( title ="Example with All-Caps Labels", subtitle ="Column labels, stub, and row groups transformed")|>opt_all_caps()
Example with All-Caps Labels
Column labels, stub, and row groups transformed
num
char
fctr
date
time
datetime
currency
grp_a
row_1
1.111e-01
apricot
one
2015-01-15
13:35
2018-01-01 02:22
49.950
row_2
2.222e+00
banana
two
2015-02-15
14:40
2018-02-02 14:33
17.950
row_3
3.333e+01
coconut
three
2015-03-15
15:45
2018-03-03 03:44
1.390
row_4
4.444e+02
durian
four
2015-04-15
16:50
2018-04-04 15:55
65100.000
grp_b
row_5
5.550e+03
NA
five
2015-05-15
17:55
2018-05-05 04:00
1325.810
row_6
NA
fig
six
2015-06-15
NA
2018-06-06 16:11
13.255
row_7
7.770e+05
grapefruit
seven
NA
19:10
2018-07-07 05:22
NA
row_8
8.880e+06
honeydew
eight
2015-08-15
20:20
NA
0.440
The transformation affects column labels, stub entries, and row group labels by default. The function also reduces font size slightly and increases weight, creating a balanced small-caps appearance.
Restricting the transformation to column labels only leaves other text unchanged, useful when row group labels or stub entries contain proper nouns or abbreviations that shouldn’t be modified.
8.4 Making interactive HTML tables
Static tables serve most publishing needs, but interactive tables offer compelling advantages for web-based reports and dashboards. Users can paginate through large datasets, sort columns to explore rankings, filter rows to find specific entries, and search globally for particular values. These capabilities transform a table from a static display into an exploratory tool.
gt provides interactive table functionality through opt_interactive(), which activates a suite of controls built on the reactable package. This integration maintains all gt formatting and styling while adding dynamic features appropriate for HTML output.
8.4.1opt_interactive()
The opt_interactive() function transforms a standard HTML table into an interactive one with pagination, sorting, filtering, and search capabilities.
towny|>dplyr::select(name, census_div, population_2021, density_2021)|>gt()|>fmt_integer(columns =c(population_2021, density_2021))|>tab_header( title ="Ontario Municipalities", subtitle ="Population and density data (2021)")|>opt_interactive()
Ontario Municipalities
Population and density data (2021)
The default configuration enables pagination (showing 10 rows per page) and column sorting. Users click column headers to sort ascending or descending. Navigation controls below the table allow moving between pages.
With use_search = TRUE, a global search box appears above the table. Users can type any text to filter rows to matching entries. The use_filters = TRUE option adds individual filter inputs below each column header, enabling column-specific filtering. The use_highlight = TRUE option highlights rows on hover, improving visual tracking.
This configuration starts with 25 rows per page and offers a dropdown for changing page size. The "jump" pagination type provides a page number input field instead of individual page buttons, useful for datasets with many pages.
Compact mode reduces vertical padding throughout the table, fitting more rows on screen. Column resizers allow users to drag column boundaries to adjust widths, accommodating different content lengths or display preferences.