8  Table styling and options

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.1 data_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.

Function Signature

data_color(
  data,
  columns = everything(),
  rows = everything(),
  direction = c("column", "row"),
  target_columns = NULL,
  method = c("auto", "numeric", "bin", "quantile", "factor"),
  palette = NULL,
  domain = NULL,
  bins = 8,
  quantiles = 4,
  levels = NULL,
  ordered = FALSE,
  na_color = NULL,
  alpha = NULL,
  reverse = FALSE,
  fn = NULL,
  apply_to = c("fill", "text"),
  autocolor_text = TRUE,
  contrast_algo = c("apca", "wcag"),
  autocolor_light = "#FFFFFF",
  autocolor_dark = "#000000"
)

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:

sp500 |>
  dplyr::filter(date >= "2015-01-01" & date <= "2015-01-15") |>
  dplyr::select(date, open, high, low, close) |>
  gt() |>
  fmt_currency(columns = c(open, high, low, close), decimals = 2) |>
  data_color(
    columns = c(open, high, low, close),
    method = "numeric",
    palette = "RdYlGn",
    domain = c(1990, 2090)
  )
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:

countrypops |>
  dplyr::filter(country_name == "United States", year >= 2000) |>
  dplyr::select(year, population) |>
  gt() |>
  fmt_integer(columns = population) |>
  data_color(
    columns = population,
    method = "bin",
    palette = "Blues",
    bins = 5
  )
year population
2000 282,162,411
2001 284,968,955
2002 287,625,193
2003 290,107,933
2004 292,805,298
2005 295,516,599
2006 298,379,912
2007 301,231,207
2008 304,093,966
2009 306,771,529
2010 309,378,227
2011 311,839,461
2012 314,339,099
2013 316,726,282
2014 319,257,560
2015 321,815,121
2016 324,353,340
2017 326,608,609
2018 328,529,577
2019 330,226,227
2020 331,577,720
2021 332,099,760
2022 334,017,321
2023 336,806,231
2024 340,110,988

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:

gtcars |>
  dplyr::select(mfr, model, hp, mpg_c) |>
  dplyr::slice_head(n = 12) |>
  gt() |>
  data_color(
    columns = hp,
    method = "quantile",
    palette = "viridis",
    quantiles = 4
  )
mfr model hp mpg_c
Ford GT 647 11
Ferrari 458 Speciale 597 13
Ferrari 458 Spider 562 13
Ferrari 458 Italia 562 13
Ferrari 488 GTB 661 15
Ferrari California 553 16
Ferrari GTC4Lusso 680 12
Ferrari FF 652 11
Ferrari F12Berlinetta 731 11
Ferrari LaFerrari 949 12
Acura NSX 573 21
Nissan GT-R 545 16

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:

gtcars |>
  dplyr::select(mfr, model, drivetrain, hp) |>
  dplyr::slice_head(n = 10) |>
  gt() |>
  data_color(
    columns = drivetrain,
    method = "factor",
    palette = c("rwd" = "#E69F00", "awd" = "#56B4E9", "4wd" = "#009E73")
  )
mfr model drivetrain hp
Ford GT rwd 647
Ferrari 458 Speciale rwd 597
Ferrari 458 Spider rwd 562
Ferrari 458 Italia rwd 562
Ferrari 488 GTB rwd 661
Ferrari California rwd 553
Ferrari GTC4Lusso awd 680
Ferrari FF awd 652
Ferrari F12Berlinetta rwd 731
Ferrari LaFerrari rwd 949

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:

countrypops |>
  dplyr::filter(country_code_3 %in% c("CHN", "IND", "USA")) |>
  dplyr::filter(year %% 10 == 0, year >= 1970) |>
  dplyr::select(country_name, year, population) |>
  dplyr::mutate(color_indicator = "") |>
  gt(groupname_col = "country_name") |>
  fmt_integer(columns = population) |>
  data_color(
    columns = population,
    target_columns = color_indicator,
    method = "numeric",
    palette = "plasma",
    domain = c(0, 1.5e9)
  ) |>
  cols_label(
    year = "Year",
    population = "Population",
    color_indicator = ""
  ) |>
  cols_width(color_indicator ~ px(30))
Year Population
China
1970 818,315,000
1980 981,235,000
1990 1,135,185,000
2000 1,262,645,000
2010 1,337,705,000
2020 1,411,100,000
India
1970 545,864,268
1980 687,354,025
1990 864,972,221
2000 1,057,922,733
2010 1,243,481,564
2020 1,402,617,695
United States
1970 205,052,000
1980 227,225,000
1990 249,623,000
2000 282,162,411
2010 309,378,227
2020 331,577,720

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:

sza |>
  dplyr::filter(latitude == 30 & tst <= "1100") |>
  dplyr::select(-latitude) |>
  dplyr::filter(!is.na(sza)) |>
  tidyr::pivot_wider(names_from = tst, values_from = sza, names_sort = TRUE) |>
  gt(rowname_col = "month") |>
  data_color(
    direction = "row",
    palette = "YlOrRd",
    na_color = "white"
  ) |>
  sub_missing(missing_text = "")
0530 0600 0630 0700 0730 0800 0830 0900 0930 1000 1030 1100
jan


89.4 83.7 78.3 73.2 68.4 64.1 60.4 57.3 55.0
feb


86.2 80.3 74.6 69.1 64.1 59.4 55.3 51.9 49.3
mar

87.5 81.1 74.9 68.8 62.9 57.4 52.2 47.5 43.5 40.3
apr
87.2 81.4 74.9 68.5 62.1 55.8 49.6 43.8 38.2 33.3 29.3
may 88.9 82.7 76.3 69.9 63.4 56.9 50.4 44.0 37.6 31.4 25.6 20.4
jun 85.3 79.2 73.1 66.8 60.4 54.0 47.5 41.0 34.5 28.1 21.7 15.7
jul 84.7 78.7 72.5 66.3 60.0 53.5 47.1 40.6 34.1 27.7 21.2 15.1
aug 87.2 81.0 74.7 68.3 61.9 55.4 48.9 42.4 36.0 29.7 23.6 18.1
sep
85.9 79.4 72.9 66.4 60.0 53.6 47.3 41.2 35.5 30.2 25.8
oct

85.1 78.7 72.4 66.2 60.1 54.4 48.8 43.8 39.5 36.1
nov


84.6 78.4 72.8 67.1 62.0 57.0 53.0 49.3 46.7
dec


88.7 82.8 77.3 72.2 67.3 63.0 59.2 56.0 53.7

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:

towny |>
  dplyr::select(name, population_2021) |>
  dplyr::slice_max(population_2021, n = 10) |>
  gt() |>
  fmt_integer(columns = population_2021) |>
  data_color(
    columns = population_2021,
    fn = scales::col_numeric(
      palette = "Greens",
      domain = c(0, 3e6),
      na.color = "gray"
    )
  )
name population_2021
Toronto 2,794,356
Ottawa 1,017,449
Mississauga 717,961
Brampton 656,480
Hamilton 569,353
London 422,324
Markham 338,503
Vaughan 323,103
Kitchener 256,885
Windsor 229,660

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.1 tab_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:

cell_text() Signature

cell_text(
  color = NULL,
  font = NULL,
  size = NULL,
  align = NULL,
  v_align = NULL,
  style = NULL,
  weight = NULL,
  stretch = NULL,
  decorate = NULL,
  transform = NULL,
  whitespace = NULL,
  indent = NULL
)

cell_fill() Signature

cell_fill(color = "#D3D3D3", alpha = NULL)

cell_borders() Signature

cell_borders(
  sides = "all",
  color = "#000000",
  style = "solid",
  weight = px(1)
)

Let’s explore these through progressively complex examples:

exibble |>
  dplyr::select(num, char, currency) |>
  gt() |>
  fmt_number(columns = num, decimals = 2) |>
  fmt_currency(columns = currency) |>
  tab_style(
    style = cell_text(weight = "bold", color = "navy"),
    locations = cells_body(columns = num, rows = num > 1000)
  )
num char currency
0.11 apricot $49.95
2.22 banana $17.95
33.33 coconut $1.39
444.40 durian $65,100.00
5,550.00 NA $1,325.81
NA fig $13.26
777,000.00 grapefruit NA
8,880,000.00 honeydew $0.44

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():

exibble |>
  dplyr::select(char, fctr, date) |>
  gt() |>
  tab_style(
    style = cell_text(
      size = px(14),
      style = "italic",
      transform = "uppercase"
    ),
    locations = cells_body(columns = char)
  )
char fctr date
apricot one 2015-01-15
banana two 2015-02-15
coconut three 2015-03-15
durian four 2015-04-15
NA five 2015-05-15
fig six 2015-06-15
grapefruit seven NA
honeydew eight 2015-08-15

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:

sp500 |>
  dplyr::filter(date >= "2015-12-01" & date <= "2015-12-15") |>
  dplyr::select(date, open, close) |>
  gt() |>
  fmt_currency(columns = c(open, close)) |>
  tab_style(
    style = list(
      cell_fill(color = "#90EE90"),
      cell_text(weight = "bold")
    ),
    locations = cells_body(rows = close > open)
  ) |>
  tab_style(
    style = list(
      cell_fill(color = "#FFB6C1"),
      cell_text(weight = "bold")
    ),
    locations = cells_body(rows = close < open)
  )
date open close
2015-12-15 $2,025.55 $2,043.41
2015-12-14 $2,013.37 $2,021.94
2015-12-11 $2,047.27 $2,012.37
2015-12-10 $2,047.93 $2,052.23
2015-12-09 $2,061.17 $2,047.62
2015-12-08 $2,073.39 $2,063.59
2015-12-07 $2,090.42 $2,077.07
2015-12-04 $2,051.24 $2,091.69
2015-12-03 $2,080.71 $2,049.62
2015-12-02 $2,101.71 $2,079.51
2015-12-01 $2,082.93 $2,102.63

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 cell_borders() helper adds border styling:

towny |>
  dplyr::filter(csd_type == "city") |>
  dplyr::select(name, population_2021, land_area_km2) |>
  dplyr::slice_max(population_2021, n = 5) |>
  gt() |>
  fmt_integer(columns = population_2021) |>
  fmt_number(columns = land_area_km2, decimals = 1) |>
  tab_style(
    style = cell_borders(
      sides = c("left", "right"),
      color = "steelblue",
      weight = px(3)
    ),
    locations = cells_body(columns = population_2021)
  )
name population_2021 land_area_km2
Toronto 2,794,356 631.1
Ottawa 1,017,449 2,788.2
Mississauga 717,961 292.7
Brampton 656,480 265.9
Hamilton 569,353 1,118.3

Heavy left and right borders visually separate the population column, drawing attention to these key figures.

Location helpers extend beyond body cells to all table components:

gtcars |>
  dplyr::select(mfr, model, year, hp, mpg_c) |>
  dplyr::slice_head(n = 6) |>
  gt() |>
  tab_header(
    title = "Select Performance Vehicles",
    subtitle = "Horsepower and fuel economy comparison"
  ) |>
  tab_style(
    style = cell_fill(color = "black"),
    locations = cells_title(groups = "title")
  ) |>
  tab_style(
    style = cell_text(color = "white"),
    locations = cells_title(groups = "title")
  ) |>
  tab_style(
    style = cell_fill(color = "gray90"),
    locations = cells_column_labels()
  )
Select Performance Vehicles
Horsepower and fuel economy comparison
mfr model year hp mpg_c
Ford GT 2017 647 11
Ferrari 458 Speciale 2015 597 13
Ferrari 458 Spider 2015 562 13
Ferrari 458 Italia 2014 562 13
Ferrari 488 GTB 2016 661 15
Ferrari California 2015 553 16

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.2 tab_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.

Function Signature

tab_style_body(
  data,
  style,
  columns = everything(),
  rows = everything(),
  values = NULL,
  pattern = NULL,
  fn = NULL,
  targets = "cell",
  extents = "body"
)

Target specific values directly:

exibble |>
  gt(rowname_col = "row", groupname_col = "group") |>
  tab_style_body(
    style = cell_fill(color = "orange"),
    values = c(49.95, 33.33)
  )
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 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:

exibble |>
  dplyr::select(char, fctr, row) |>
  gt() |>
  tab_style_body(
    style = cell_text(color = "darkgreen", weight = "bold"),
    pattern = "^a"
  )
char fctr row
apricot one row_1
banana two row_2
coconut three row_3
durian four row_4
NA five row_5
fig six row_6
grapefruit seven row_7
honeydew eight row_8

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:

exibble |>
  gt(rowname_col = "row", groupname_col = "group") |>
  tab_style_body(
    columns = where(is.numeric),
    style = cell_fill(color = "lightblue"),
    fn = function(x) !is.na(x) && x > 0 && x < 100
  )
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 function receives each cell value and returns TRUE for cells that should be styled. Here, we highlight positive numeric values under 100.

Styling NA values is a common use case:

exibble |>
  gt() |>
  tab_style_body(
    style = cell_text(color = "red", style = "italic"),
    fn = function(x) is.na(x)
  ) |>
  sub_missing(missing_text = "Missing")
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 Missing five 2015-05-15 17:55 2018-05-05 04:00 1325.810 row_5 grp_b
Missing fig six 2015-06-15 Missing 2018-06-06 16:11 13.255 row_6 grp_b
7.770e+05 grapefruit seven Missing 19:10 2018-07-07 05:22 Missing row_7 grp_b
8.880e+06 honeydew eight 2015-08-15 20:20 Missing 0.440 row_8 grp_b

The targets argument extends styling beyond individual cells to entire rows or columns:

exibble |>
  gt(rowname_col = "row", groupname_col = "group") |>
  tab_style_body(
    style = cell_fill(color = "lightyellow"),
    values = 49.95,
    targets = "row"
  )
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

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:

exibble |>
  gt(rowname_col = "row", groupname_col = "group") |>
  tab_style_body(
    style = cell_fill(color = "lightblue"),
    values = 49.95,
    targets = "row",
    extents = c("body", "stub")
  )
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

Including "stub" in extents means the row label also receives the styling, creating visual continuity across the entire row.

8.2.3 opt_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.1 tab_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.

Function Signature (Selected Parameters)

tab_options(
  data,
  # Table-level settings
  table.width = NULL,
  table.layout = NULL,
  table.align = NULL,
  table.background.color = NULL,
  table.font.names = NULL,
  table.font.size = NULL,
  table.font.color = NULL,
  table.border.top.style = NULL,
  table.border.top.width = NULL,
  table.border.top.color = NULL,
  
  # Heading settings  
  heading.background.color = NULL,
  heading.align = NULL,
  heading.title.font.size = NULL,
  heading.padding = NULL,
  
  # Column labels settings
  column_labels.background.color = NULL,
  column_labels.font.size = NULL,
  column_labels.font.weight = NULL,
  column_labels.border.top.style = NULL,
  column_labels.border.bottom.style = NULL,
  
  # Row group settings
  row_group.background.color = NULL,
  row_group.font.weight = NULL,
  row_group.padding = NULL,
  
  # Table body settings
  table_body.hlines.style = NULL,
  table_body.hlines.color = NULL,
  table_body.vlines.style = NULL,
  data_row.padding = NULL,
  
  # Stub settings
  stub.background.color = NULL,
  stub.font.weight = NULL,
  stub.border.style = NULL,
  
  # Summary row settings
  summary_row.background.color = NULL,
  grand_summary_row.background.color = NULL,
  
  # Footer settings
  footnotes.font.size = NULL,
  footnotes.padding = NULL,
  source_notes.font.size = NULL,
  
  # Row striping
  row.striping.background_color = NULL,
  row.striping.include_table_body = NULL,
  
  # Container settings (HTML)
  container.width = NULL,
  container.height = NULL,
  
  # And many more...
)

Let’s begin with foundational table-level options:

exibble |>
  dplyr::select(num, char, currency, date) |>
  gt() |>
  tab_header(
    title = "Customized Table Appearance",
    subtitle = "Demonstrating table-level options"
  ) |>
  tab_options(
    table.width = pct(100),
    table.background.color = "ivory",
    table.font.size = px(14),
    table.border.top.style = "solid",
    table.border.top.width = px(3),
    table.border.top.color = "darkblue"
  )
Customized Table Appearance
Demonstrating table-level options
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

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:

gtcars |>
  dplyr::select(mfr, model, year, hp, mpg_c) |>
  dplyr::slice_head(n = 8) |>
  gt() |>
  tab_header(
    title = "High-Performance Vehicles",
    subtitle = "Selected specifications"
  ) |>
  tab_options(
    heading.background.color = "steelblue",
    heading.title.font.size = px(20),
    heading.subtitle.font.size = px(14),
    heading.padding = px(10),
    column_labels.background.color = "lightsteelblue",
    column_labels.font.weight = "bold",
    column_labels.padding = px(8)
  )
High-Performance Vehicles
Selected specifications
mfr model year hp mpg_c
Ford GT 2017 647 11
Ferrari 458 Speciale 2015 597 13
Ferrari 458 Spider 2015 562 13
Ferrari 458 Italia 2014 562 13
Ferrari 488 GTB 2016 661 15
Ferrari California 2015 553 16
Ferrari GTC4Lusso 2017 680 12
Ferrari FF 2015 652 11

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:

exibble |>
  gt(rowname_col = "row", groupname_col = "group") |>
  tab_options(
    table_body.hlines.color = "gray90",
    row_group.background.color = "gray95",
    row_group.font.weight = "bold",
    stub.background.color = "gray98",
    stub.font.weight = "bold",
    data_row.padding = px(4)
  )
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

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:

# LaTeX-specific options (not evaluated)
gt_table |>
  tab_options(
    latex.use_longtable = TRUE,
    latex.header_repeat = TRUE,
    latex.tbl.pos = "h"
  )

These settings enable multi-page tables with repeating headers and control float positioning.

8.3.2 opt_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
)

Using a Google Font:

sp500 |>
  dplyr::filter(date >= "2015-01-01") |>
  dplyr::slice_head(n = 8) |>
  dplyr::select(date, open, high, low, close) |>
  gt() |>
  fmt_currency(columns = c(open, high, low, close)) |>
  opt_table_font(
    font = list(
      google_font(name = "IBM Plex Sans"),
      "Helvetica", "Arial", "sans-serif"
    )
  )
date open high low close
2015-12-31 $2,060.59 $2,062.54 $2,043.62 $2,043.94
2015-12-30 $2,077.34 $2,077.34 $2,061.97 $2,063.36
2015-12-29 $2,060.54 $2,081.56 $2,060.54 $2,078.36
2015-12-28 $2,057.77 $2,057.77 $2,044.20 $2,056.50
2015-12-24 $2,063.52 $2,067.36 $2,058.73 $2,060.99
2015-12-23 $2,042.20 $2,064.73 $2,042.20 $2,064.29
2015-12-22 $2,023.15 $2,042.74 $2,020.49 $2,038.97
2015-12-21 $2,010.27 $2,022.90 $2,005.93 $2,021.15

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:

exibble |>
  dplyr::select(num, char, currency) |>
  gt() |>
  opt_table_font(stack = "rounded-sans")
num char currency
1.111e-01 apricot 49.950
2.222e+00 banana 17.950
3.333e+01 coconut 1.390
4.444e+02 durian 65100.000
5.550e+03 NA 1325.810
NA fig 13.255
7.770e+05 grapefruit NA
8.880e+06 honeydew 0.440

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.

Combining font with other properties:

gtcars |>
  dplyr::select(mfr, model, hp, mpg_c) |>
  dplyr::slice_head(n = 6) |>
  gt() |>
  opt_table_font(
    stack = "monospace-code",
    size = px(13),
    weight = "normal",
    color = "gray30"
  )
mfr model hp mpg_c
Ford GT 647 11
Ferrari 458 Speciale 597 13
Ferrari 458 Spider 562 13
Ferrari 458 Italia 562 13
Ferrari 488 GTB 661 15
Ferrari California 553 16

The monospace stack with specified size, weight, and color creates a technical, code-like appearance appropriate for certain data types.

8.3.3 opt_horizontal_padding() and opt_vertical_padding()

These functions provide quick control over table density by scaling all padding values proportionally.

Function Signatures

opt_horizontal_padding(data, scale = 1)
opt_vertical_padding(data, scale = 1)

The scale parameter accepts values from 0 to 3, where 1 is the default. Values below 1 compress the table while values above 1 expand it.

Creating a compact table:

exibble |>
  gt(rowname_col = "row", groupname_col = "group") |>
  tab_header(title = "Compact Table") |>
  opt_vertical_padding(scale = 0.3) |>
  opt_horizontal_padding(scale = 0.5)
Compact Table
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

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.

Creating a spacious table:

exibble |>
  dplyr::select(num, char, date) |>
  dplyr::slice_head(n = 4) |>
  gt() |>
  tab_header(title = "Spacious Table") |>
  opt_vertical_padding(scale = 2) |>
  opt_horizontal_padding(scale = 2.5)
Spacious Table
num char date
0.1111 apricot 2015-01-15
2.2220 banana 2015-02-15
33.3300 coconut 2015-03-15
444.4000 durian 2015-04-15

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.4 opt_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():

towny |>
  dplyr::select(name, population_2021, land_area_km2) |>
  dplyr::slice_max(population_2021, n = 10) |>
  gt() |>
  fmt_integer(columns = population_2021) |>
  fmt_number(columns = land_area_km2, decimals = 1) |>
  opt_row_striping() |>
  tab_options(
    row.striping.background_color = "#E8F4F8",
    row.striping.include_stub = TRUE
  )
name population_2021 land_area_km2
Toronto 2,794,356 631.1
Ottawa 1,017,449 2,788.2
Mississauga 717,961 292.7
Brampton 656,480 265.9
Hamilton 569,353 1,118.3
London 422,324 420.5
Markham 338,503 210.9
Vaughan 323,103 272.4
Kitchener 256,885 136.8
Windsor 229,660 146.0

The light blue striping with stub inclusion creates a cohesive visual treatment across the entire row.

8.3.5 opt_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.

Function Signature

opt_all_caps(
  data,
  all_caps = TRUE,
  locations = c("column_labels", "stub", "row_group")
)
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.

Selective application:

gtcars |>
  dplyr::select(mfr, model, year, hp) |>
  dplyr::slice_head(n = 6) |>
  gt() |>
  opt_all_caps(locations = "column_labels")
mfr model year hp
Ford GT 2017 647
Ferrari 458 Speciale 2015 597
Ferrari 458 Spider 2015 562
Ferrari 458 Italia 2014 562
Ferrari 488 GTB 2016 661
Ferrari California 2015 553

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.1 opt_interactive()

The opt_interactive() function transforms a standard HTML table into an interactive one with pagination, sorting, filtering, and search capabilities.

Function Signature

opt_interactive(
  data,
  active = TRUE,
  use_pagination = TRUE,
  use_pagination_info = TRUE,
  use_sorting = TRUE,
  use_search = FALSE,
  use_filters = FALSE,
  use_resizers = FALSE,
  use_highlight = FALSE,
  use_compact_mode = FALSE,
  use_text_wrapping = TRUE,
  use_page_size_select = FALSE,
  page_size_default = 10,
  page_size_values = c(10, 25, 50, 100),
  pagination_type = c("numbers", "jump", "simple"),
  height = "auto",
  selection_mode = NULL
)

A basic interactive table:

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.

Enabling search and filtering:

gtcars |>
  dplyr::select(mfr, model, year, hp, mpg_c, msrp) |>
  gt() |>
  fmt_currency(columns = msrp, decimals = 0) |>
  tab_header(title = "Performance Vehicles Database") |>
  opt_interactive(
    use_search = TRUE,
    use_filters = TRUE,
    use_highlight = TRUE
  )
Performance Vehicles Database

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.

Customizing pagination:

countrypops |>
  dplyr::filter(year >= 2010) |>
  dplyr::select(country_name, year, population) |>
  gt() |>
  fmt_integer(columns = population) |>
  opt_interactive(
    use_pagination = TRUE,
    use_page_size_select = TRUE,
    page_size_default = 25,
    page_size_values = c(10, 25, 50, 100, 200),
    pagination_type = "jump"
  )

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 and column resizing:

pizzaplace |>
  dplyr::select(id, date, time, name, size, price) |>
  dplyr::slice_head(n = 100) |>
  gt() |>
  fmt_currency(columns = price) |>
  opt_interactive(
    use_compact_mode = TRUE,
    use_resizers = TRUE,
    page_size_default = 15
  )

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.

Interactive tables maintain gt styling:

sp500 |>
  dplyr::select(date, open, high, low, close, volume) |>
  gt() |>
  fmt_currency(columns = c(open, high, low, close)) |>
  fmt_integer(columns = volume) |>
  data_color(
    columns = volume,
    palette = "Blues"
  ) |>
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) |>
  opt_interactive(
    use_search = TRUE,
    use_sorting = TRUE,
    use_highlight = TRUE
  )

Formatting from fmt_*() functions, colors from data_color(), and styles from tab_style() all persist in the interactive version. This allows you to build a fully-styled table and then add interactivity as a final step.

For Shiny applications, the selection_mode argument enables row selection:

# Shiny context (not evaluated)
output$my_table <- render_gt({
  my_data |>
    gt() |>
    opt_interactive(
      selection_mode = "multiple"
    )
})

With selection_mode = "single" or "multiple", users can click rows to select them, and the selection state becomes available to Shiny’s reactive system.

A note on limitations: interactive tables are HTML-only features. When rendering to PDF, RTF, Word, or other formats, opt_interactive() has no effect and tables render in their static form. Plan accordingly when creating documents destined for multiple output formats.

The combination of gt’s sophisticated formatting capabilities with interactive exploration creates tables that serve both presentation and analysis purposes. Readers can appreciate the careful styling while still drilling into the data to answer their own questions: a powerful synthesis for modern data communication.

8.5 Summary

This chapter has explored the extensive styling and customization options that can transform functional tables into visually-compelling presentations. From data-driven colorization to precise typographic control, gt provides the tools to achieve virtually any aesthetic goal.

The key capabilities we’ve covered:

  • data-driven coloring: data_color() maps values to colors using palettes, gradients, or custom scales. This visual encoding helps readers identify patterns, outliers, and relationships at a glance.
  • precision styling: tab_style() applies CSS-like styling to any table location using the cells_*() helper functions. Combined with cell_text(), cell_fill(), and cell_borders(), you have complete control over typography, backgrounds, and borders.
  • global options: tab_options() provides access to dozens of table-wide settings, from font families to padding to border styles. These options establish consistent defaults that individual styles can override.
  • convenience functions: The opt_*() family offers quick access to common configurations: opt_stylize() for preset themes, opt_row_striping() for zebra stripes, opt_table_font() for typography, and many more.
  • interactive tables: opt_interactive() transforms static tables into explorable interfaces with sorting, filtering, searching, and pagination (all while preserving your formatting and styling choices).

Effective table styling serves communication, not decoration. Color should highlight meaningful distinctions. Typography should establish hierarchy. Borders should delineate structure. The best-styled tables are those where readers don’t notice the styling much at all, they simply find the information they need with minimal effort.

The next chapter covers footnotes and source notes, which add supplementary information to your tables without cluttering the main presentation. You’ll learn to attach explanatory notes to specific cells, define footnote marks, and create properly formatted citations.