9  Footnotes and source notes

Tables often need to convey more than what fits neatly in rows and columns. A value might require clarification. A column heading might benefit from a definition. The data source might need acknowledgment. Footnotes and source notes address these needs, providing a structured way to add supplementary information without cluttering the main table body.

In gt, the footer section accommodates two types of annotations: footnotes (linked to specific table locations via marks) and source notes (general citations or commentary). This chapter explores how to add both, how to target specific table locations for footnote attachment, and how to customize the appearance of footnote marks and the footer layout.

9.2 Adding footnotes

9.2.1 tab_footnote() and the cells_*() location helpers

The tab_footnote() function can make it a painless process to add a footnote to a gt table. There are commonly two components to a footnote: (1) a footnote mark that is attached to the targeted cell content, and (2) the footnote text itself that is placed in the table’s footer area. Each unit of footnote text in the footer is linked to an element of text or otherwise through the footnote mark. The footnote system in gt presents footnotes in a way that matches the usual expectations, where:

  1. footnote marks have a sequence, whether they are symbols, numbers, or letters

  2. multiple footnotes can be applied to the same content (and marks are always presented in an ordered fashion)

  3. footnote text in the footer is never exactly repeated, gt reuses footnote marks where needed throughout the table

  4. footnote marks are ordered across the table in a consistent manner (left to right, top to bottom)

tab_footnote(
  data,
  footnote,
  locations = NULL,
  placement = c("auto", "right", "left")
)

footnote: Footnote text

scalar<character> // required

The text to be used in the footnote. We can optionally use the md() and html() functions to style the text as Markdown or to retain HTML elements in the footnote text.


locations: Locations to target

<locations expressions> // default: NULL, so optional

The cell or set of cells to be associated with the footnote. Supplying any of the cells_*() helper functions is a useful way to target the location cells that are associated with the footnote text. Additionally, we can enclose several cells_*() calls within a list() if we wish to link the footnote text to different types of locations (e.g., body cells, row group labels, the table title, etc.).


placement: Placement of footnote mark

singl-kw:[auto|right|left] // default: "auto"

Where to affix footnote marks to the table content. Two options for this are "left" or "right", where the placement is either to the absolute left or right of the cell content. By default, however, this option is set to "auto" whereby gt will choose a preferred left-or-right placement depending on the alignment of the cell content.


Using a subset of the sza dataset, let’s create a new gt table. The sza column will get all of its cells filled with a background color according to data values (this is done with data_color()) and then the use of tab_footnote() lets us add a footnote to the sza column label (explaining what the color gradient signifies).

sza |>
  dplyr::filter(
    latitude == 20 &
      month == "jan" &
      !is.na(sza)
  ) |>
  dplyr::select(-latitude, -month) |>
  gt() |>
  data_color(
    columns = sza,
    palette = c("white", "yellow", "navyblue"),
    domain = c(0, 90)
  ) |>
  tab_footnote(
    footnote = "Color indicates the solar zenith angle.",
    locations = cells_column_labels(columns = sza)
  )
tst sza1
0700 84.9
0730 78.7
0800 72.7
0830 66.1
0900 61.5
0930 56.5
1000 52.1
1030 48.3
1100 45.5
1130 43.6
1200 43.0
1 Color indicates the solar zenith angle.

Of course, we can add more than one footnote to the table, but, we have to use several calls of tab_footnote(). This variation of the sza table has three footnotes: one on the "TST" column label and two on the "SZA" column label (these were capitalized with opt_all_caps()). We have three calls of tab_footnote() and while the order of calls usually doesn’t matter, it does have a subtle effect here since two footnotes are associated with the same text content (try reversing the second and third calls and observe the effect in the footer).

sza |>
  dplyr::filter(
    latitude == 20 &
      month == "jan" &
      !is.na(sza)
  ) |>
  dplyr::select(-latitude, -month) |>
  gt() |>
  opt_all_caps() |>
  cols_align(align = "center") |>
  cols_width(everything() ~ px(200)) |>
  tab_footnote(
    footnote = md("TST stands for *True Solar Time*."),
    locations = cells_column_labels(columns = tst)
  ) |>
  tab_footnote(
    footnote = md("SZA stands for *Solar Zenith Angle*."),
    locations = cells_column_labels(columns = sza)
  ) |>
  tab_footnote(
    footnote = "Higher Values indicate sun closer to horizon.",
    locations = cells_column_labels(columns = sza)
  ) |>
  tab_options(footnotes.multiline = FALSE)
tst1 sza2,3
0700 84.9
0730 78.7
0800 72.7
0830 66.1
0900 61.5
0930 56.5
1000 52.1
1030 48.3
1100 45.5
1130 43.6
1200 43.0
1 TST stands for True Solar Time. 2 SZA stands for Solar Zenith Angle. 3 Higher Values indicate sun closer to horizon.

Text in the footer (both from footnotes and also from source notes) tends to widen the table and all the columns within in. We can limit that setting column widths, which is what was done above with cols_width(). There can also be a correspondingly large amount of vertical space taken up by the footer since footnotes will, by default, each start on a new line. In the above example, we used tab_options(footnotes.multiline = FALSE) to make it so that all footer text is contained in a single block of text.

Let’s move on to another footnote-laden table, this one based on the towny dataset. We have a header part, with a title and a subtitle. We can choose which of these could be associated with a footnote and in this case it is the "subtitle" (one of two options in the cells_title() helper function). This table has a stub with row labels and some of those labels are associated with a footnote. So long as row labels are unique, they can be easily used as row identifiers in cells_stub(). The third footnote is placed on the "Density" column label. Here, changing the order of the tab_footnote() calls has no effect on the final table rendering.

towny |>
  dplyr::filter(csd_type == "city") |>
  dplyr::arrange(desc(population_2021)) |>
  dplyr::select(name, density_2021, population_2021) |>
  dplyr::slice_head(n = 10) |>
  gt(rowname_col = "name") |>
  tab_header(
    title = md("The 10 Largest Municipalities in `towny`"),
    subtitle = "Population values taken from the 2021 census."
  ) |>
  fmt_integer() |>
  cols_label(
    density_2021 = "Density",
    population_2021 = "Population"
  ) |>
  tab_footnote(
    footnote = "Part of the Greater Toronto Area.",
    locations = cells_stub(rows = c(
      "Toronto", "Mississauga", "Brampton", "Markham", "Vaughan"
    ))
  ) |>
  tab_footnote(
    footnote = md("Density is in terms of persons per km^2^."),
    locations = cells_column_labels(columns = density_2021)
  ) |>
  tab_footnote(
    footnote = "Census results made public on February 9, 2022.",
    locations = cells_title(groups = "subtitle")
  ) |>
  tab_source_note(source_note = md(
    "Data taken from the `towny` dataset (in the **gt** package)."
  )) |>
  opt_footnote_marks(marks = "letters")
The 10 Largest Municipalities in towny
Population values taken from the 2021 census.a
Densityb Population
Torontoc 4,428 2,794,356
Ottawa 365 1,017,449
Mississaugac 2,453 717,961
Bramptonc 2,469 656,480
Hamilton 509 569,353
London 1,004 422,324
Markhamc 1,605 338,503
Vaughanc 1,186 323,103
Kitchener 1,878 256,885
Windsor 1,573 229,660
a Census results made public on February 9, 2022.
b Density is in terms of persons per km2.
c Part of the Greater Toronto Area.
Data taken from the towny dataset (in the gt package).

In the above table, we elected to change the footnote marks to letters instead of the default numbers (done through opt_footnote_marks()). A source note was also added; this was mainly to demonstrate that source notes will be positioned beneath footnotes in the footer section.

For our final example, let’s make a relatively small table deriving from the sp500 dataset. The set of tab_footnote() calls used here (four of them) have minor variations that allow for interesting expressions of footnotes. Two of the footnotes target values in the body of the table (using the cells_body() helper function to achieve this). On numeric values that right-aligned, gt will opt to place the footnote on the left of the content so as to not disrupt the alignment. However, the placement argument can be used to force the positioning of the footnote mark after the content.

We can also opt to include footnotes that have no associated footnote marks whatsoever. This is done by not providing anything to locations. These ‘markless’ footnotes will precede the other footnotes in the footer section.

sp500 |>
  dplyr::filter(date >= "2015-01-05" & date <="2015-01-10") |>
  dplyr::select(-c(adj_close, volume, high, low)) |>
  dplyr::mutate(change = close - open) |>
  dplyr::arrange(date) |>
  gt() |>
  tab_header(title = "S&P 500") |>
  fmt_date(date_style = "m_day_year") |>
  fmt_currency() |>
  cols_width(everything() ~ px(150)) |>
  tab_footnote(
    footnote = "More red days than green in this period.",
    locations = cells_column_labels(columns = change)
  ) |>
  tab_footnote(
    footnote = "Lowest opening value.",
    locations = cells_body(columns = open, rows = 3),
  ) |>
  tab_footnote(
    footnote = "Devastating losses on this day.",
    locations = cells_body(columns = change, rows = 1),
    placement = "right"
  ) |>
  tab_footnote(footnote = "All values in USD.") |>
  opt_footnote_marks(marks = "LETTERS") |>
  opt_footnote_spec(spec_ref = "i[x]", spec_ftr = "x.")
S&P 500
date open close change[A]
Jan 5, 2015 $2,054.44 $2,020.58 −$33.86[B]
Jan 6, 2015 $2,022.15 $2,002.61 −$19.54
Jan 7, 2015 [C] $2,005.55 $2,025.90 $20.35
Jan 8, 2015 $2,030.61 $2,062.14 $31.53
Jan 9, 2015 $2,063.45 $2,044.81 −$18.64
All values in USD.
A. More red days than green in this period.
B. Devastating losses on this day.
C. Lowest opening value.

Aside from changing the footnote marks to be "LETTERS", we’ve also changed the way the marks are formatted. In opt_footnote_spec() the spec_ref option, governing the footnote marks across the table, describes marks that are italicized and set between square brackets ("i[x]"). The spec_ftr argument is for the footer representation of the footnote marks and as described in the call with "x.", it’ll be the mark followed by a period.

9.3 Location helpers for footnotes

The power of tab_footnote() lies in its ability to target virtually any part of the table. The locations argument accepts various cells_*() helper functions, each designed to select specific table regions.

9.3.1 Targeting column labels with cells_column_labels()

Column labels are common footnote targets and they often benefit from definitions or clarifications:

gtcars |>
  dplyr::select(mfr, model, hp, mpg_c, mpg_h) |>
  dplyr::slice_head(n = 5) |>
  gt() |>
  cols_label(
    mfr = "Manufacturer",
    model = "Model",
    hp = "Horsepower",
    mpg_c = "City MPG",
    mpg_h = "Highway MPG"
  ) |>
  tab_footnote(
    footnote = "Miles per gallon in city driving conditions.",
    locations = cells_column_labels(columns = mpg_c)
  ) |>
  tab_footnote(
    footnote = "Miles per gallon on highway.",
    locations = cells_column_labels(columns = mpg_h)
  )
Manufacturer Model Horsepower City MPG1 Highway MPG2
Ford GT 647 11 18
Ferrari 458 Speciale 597 13 17
Ferrari 458 Spider 562 13 17
Ferrari 458 Italia 562 13 17
Ferrari 488 GTB 661 15 22
1 Miles per gallon in city driving conditions.
2 Miles per gallon on highway.

Each column label receives its own footnote mark, and the footer displays both explanations. The columns argument in cells_column_labels() accepts column names, indices, or tidyselect helpers.

9.3.2 Targeting multiple columns at once

When several columns share a common characteristic, you can attach one footnote to all of them:

gtcars |>
  dplyr::select(mfr, model, mpg_c, mpg_h) |>
  dplyr::slice_head(n = 5) |>
  gt() |>
  cols_label(
    mfr = "Manufacturer",
    model = "Model",
    mpg_c = "City",
    mpg_h = "Highway"
  ) |>
  tab_spanner(
    label = "Fuel Economy (MPG)",
    columns = starts_with("mpg")
  ) |>
  tab_footnote(
    footnote = "EPA estimated values.",
    locations = cells_column_labels(columns = c(mpg_c, mpg_h))
  )
Manufacturer Model
Fuel Economy (MPG)
City1 Highway1
Ford GT 11 18
Ferrari 458 Speciale 13 17
Ferrari 458 Spider 13 17
Ferrari 458 Italia 13 17
Ferrari 488 GTB 15 22
1 EPA estimated values.

Both “City” and “Highway” column labels receive the same footnote mark, and only one entry appears in the footer. This demonstrates gt’s intelligent handling of duplicate footnotes.

9.3.3 Targeting body cells with cells_body()

Footnotes can attach to specific data values when individual cells need explanation:

exibble |>
  dplyr::select(row, num, currency) |>
  gt(rowname_col = "row") |>
  fmt_number(columns = num, decimals = 2) |>
  fmt_currency(columns = currency) |>
  tab_footnote(
    footnote = "Highest value in dataset.",
    locations = cells_body(columns = num, rows = num == max(num, na.rm = TRUE))
  ) |>
  tab_footnote(
    footnote = "Missing data point.",
    locations = cells_body(columns = num, rows = is.na(num))
  )
num currency
row_1 0.11 $49.95
row_2 2.22 $17.95
row_3 33.33 $1.39
row_4 444.40 $65,100.00
row_5 5,550.00 $1,325.81
row_6 1 NA $13.26
row_7 777,000.00 NA
row_8 2 8,880,000.00 $0.44
1 Missing data point.
2 Highest value in dataset.

The rows argument accepts logical conditions, row numbers, or row names. Here, we identify cells programmatically: one by value comparison, another by detecting missingness.

9.3.4 Targeting stub cells with cells_stub()

When rows have labels (a stub column), those labels can receive footnotes:

pizzaplace |>
  dplyr::count(type, name = "orders") |>
  dplyr::arrange(desc(orders)) |>
  gt(rowname_col = "type") |>
  fmt_integer(columns = orders) |>
  tab_header(title = "Pizza Orders by Type") |>
  tab_footnote(
    footnote = "Traditional Italian-American style.",
    locations = cells_stub(rows = "classic")
  ) |>
  tab_footnote(
    footnote = "Specialty recipes with premium toppings.",
    locations = cells_stub(rows = "supreme")
  )
Pizza Orders by Type
orders
classic1 14,888
supreme2 11,987
veggie 11,649
chicken 11,050
1 Traditional Italian-American style.
2 Specialty recipes with premium toppings.

Row labels in the stub serve as identifiers, so you can reference them directly by name.

9.3.5 Targeting row group labels with cells_row_groups()

When tables have grouped rows, the group labels can also receive footnotes:

gtcars |>
  dplyr::select(mfr, model, year, hp) |>
  dplyr::filter(mfr %in% c("Ferrari", "Lamborghini", "Maserati")) |>
  gt(rowname_col = "model", groupname_col = "mfr") |>
  tab_footnote(
    footnote = "Part of the Fiat Chrysler Automobiles group.",
    locations = cells_row_groups(groups = c("Ferrari", "Maserati"))
  )
year hp
Ferrari1
458 Speciale 2015 597
458 Spider 2015 562
458 Italia 2014 562
488 GTB 2016 661
California 2015 553
GTC4Lusso 2017 680
FF 2015 652
F12Berlinetta 2015 731
LaFerrari 2015 949
Lamborghini
Aventador 2015 700
Huracan 2015 610
Gallardo 2014 550
Maserati1
Granturismo 2016 454
Quattroporte 2016 404
Ghibli 2016 345
1 Part of the Fiat Chrysler Automobiles group.

Multiple row groups can share the same footnote when they have something in common.

9.3.6 Targeting titles with cells_title()

The header section, containing a title and a subtitle, can also host footnotes:

countrypops |>
  dplyr::filter(
    country_name %in% c("United States", "China", "India"),
    year == 2022
  ) |>
  dplyr::select(country_name, population) |>
  gt(rowname_col = "country_name") |>
  fmt_number(columns = population, suffixing = TRUE) |>
  tab_header(
    title = "World's Most Populous Countries",
    subtitle = "Population estimates for 2022"
  ) |>
  tab_footnote(
    footnote = "Based on UN World Population Prospects.",
    locations = cells_title(groups = "subtitle")
  )
World's Most Populous Countries
Population estimates for 20221
population
China 1.41B
India 1.43B
United States 334.02M
1 Based on UN World Population Prospects.

The groups argument specifies whether to target "title" or "subtitle".

9.3.7 Targeting column spanners with cells_column_spanners()

Column spanners, labels that span multiple columns, can receive footnotes too:

illness |>
  dplyr::slice_head(n = 5) |>
  dplyr::select(test, units, day_3, day_4, day_5) |>
  gt(rowname_col = "test") |>
  tab_spanner(
    label = "Mid-Week Measurements",
    columns = starts_with("day")
  ) |>
  cols_label(
    units = "Units",
    day_3 = "Day 3",
    day_4 = "Day 4", 
    day_5 = "Day 5"
  ) |>
  tab_footnote(
    footnote = "Values collected between 8-10am daily.",
    locations = cells_column_spanners(spanners = "Mid-Week Measurements")
  )
Units
Mid-Week Measurements1
Day 3 Day 4 Day 5
Viral load copies per mL 12000.00 4200.00 1600.00
WBC x10^9 / L 5.26 4.26 9.92
Neutrophils x10^9 / L 4.87 4.72 7.92
RBC x10^12 / L 5.72 5.98 4.23
Hb g / L 153.00 135.00 126.00
1 Values collected between 8-10am daily.

The spanners argument takes the spanner label text.

9.3.8 Targeting the stubhead with cells_stubhead()

If your table has both a stub and a stubhead label, that label can receive footnotes:

towny |>
  dplyr::select(name, population_2021, density_2021) |>
  dplyr::slice_max(population_2021, n = 5) |>
  gt(rowname_col = "name") |>
  tab_stubhead(label = "Municipality") |>
  fmt_integer() |>
  cols_label(
    population_2021 = "Population",
    density_2021 = "Density"
  ) |>
  tab_footnote(
    footnote = "Census subdivisions classified as cities or towns.",
    locations = cells_stubhead()
  )
Municipality1 Population Density
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
1 Census subdivisions classified as cities or towns.

The cells_stubhead() helper takes no arguments as there’s only one stubhead per table.

9.3.9 Targeting summary rows

Summary rows (created with summary_rows() or grand_summary_rows()) can receive footnotes using cells_summary(), cells_grand_summary(), cells_stub_summary(), or cells_stub_grand_summary():

pizzaplace |>
  dplyr::filter(date <= "2015-01-07") |>
  dplyr::summarize(
    revenue = sum(price),
    orders = dplyr::n(),
    .by = date
  ) |>
  gt(rowname_col = "date") |>
  fmt_date(columns = stub(), date_style = "yMMMd") |>
  fmt_currency(columns = revenue) |>
  fmt_integer(columns = orders) |>
  grand_summary_rows(
    fns = list(
      Total ~ sum(.),
      Average ~ mean(.)
    ),
    fmt = ~ fmt_number(., decimals = 0, use_seps = TRUE)
  ) |>
  tab_footnote(
    footnote = "Simple arithmetic mean across days.",
    locations = cells_grand_summary(columns = revenue, rows = "Average")
  )
revenue orders
Jan 1, 2015 $2,713.85 162
Jan 2, 2015 $2,731.90 165
Jan 3, 2015 $2,662.40 158
Jan 4, 2015 $1,755.45 106
Jan 5, 2015 $2,065.95 125
Jan 6, 2015 $2,428.95 147
Jan 7, 2015 $2,202.20 138
Total 16,561 1,001
Average 2,3661 143
1 Simple arithmetic mean across days.

9.3.10 Combining multiple locations

A single footnote can attach to multiple locations by wrapping cells_*() calls in a list:

exibble |>
  dplyr::select(row, num, currency, group) |>
  gt(rowname_col = "row", groupname_col = "group") |>
  fmt_number(columns = num, decimals = 1) |>
  fmt_currency(columns = currency) |>
  tab_footnote(
    footnote = "Numeric measurements in consistent units.",
    locations = list(
      cells_column_labels(columns = num),
      cells_column_labels(columns = currency)
    )
  )
num1 currency1
grp_a
row_1 0.1 $49.95
row_2 2.2 $17.95
row_3 33.3 $1.39
row_4 444.4 $65,100.00
grp_b
row_5 5,550.0 $1,325.81
row_6 NA $13.26
row_7 777,000.0 NA
row_8 8,880,000.0 $0.44
1 Numeric measurements in consistent units.

Both column labels receive the same footnote mark, consolidating related information.

9.4 Unmarked footnotes

Sometimes you want footer text without a corresponding mark in the table. Omitting the locations argument creates an unmarked footnote:

gtcars |>
  dplyr::select(mfr, model, year, msrp) |>
  dplyr::slice_head(n = 5) |>
  gt() |>
  fmt_currency(columns = msrp, decimals = 0) |>
  cols_label(
    mfr = "Make",
    model = "Model",
    year = "Year",
    msrp = "MSRP"
  ) |>
  tab_footnote(footnote = "All prices in US dollars.") |>
  tab_footnote(footnote = "Data current as of 2016.")
Make Model Year MSRP
Ford GT 2017 $447,000
Ferrari 458 Speciale 2015 $291,744
Ferrari 458 Spider 2015 $263,553
Ferrari 458 Italia 2014 $233,509
Ferrari 488 GTB 2016 $245,400
All prices in US dollars.
Data current as of 2016.

Unmarked footnotes appear in the footer without any superscript reference. They’re useful for general notes that apply to the entire table.

9.5 Footnote mark placement

The placement argument controls where the footnote mark appears relative to cell content:

sp500 |>
  dplyr::filter(date >= "2015-01-05" & date <= "2015-01-09") |>
  dplyr::select(date, open, close) |>
  dplyr::mutate(change = close - open) |>
  gt() |>
  fmt_date(columns = date, date_style = "yMMMd") |>
  fmt_currency(columns = c(open, close, change)) |>
  tab_footnote(
    footnote = "Largest daily loss.",
    locations = cells_body(columns = change, rows = 1),
    placement = "right"
  ) |>
  tab_footnote(
    footnote = "Smallest opening price.",
    locations = cells_body(columns = open, rows = 3),
    placement = "left"
  )
date open close change
Jan 9, 2015 $2,063.45 $2,044.81 −$18.641
Jan 8, 2015 $2,030.61 $2,062.14 $31.53
Jan 7, 2015 2 $2,005.55 $2,025.90 $20.35
Jan 6, 2015 $2,022.15 $2,002.61 −$19.54
Jan 5, 2015 $2,054.44 $2,020.58 −$33.86
1 Largest daily loss.
2 Smallest opening price.

By default (placement = "auto"), gt places marks on the left for right-aligned content (to preserve numeric alignment) and on the right for left-aligned content. You can override this with explicit "left" or "right" values.

9.6 Source notes with tab_source_note()

Source notes provide citations or general commentary without linking to specific table locations. They appear beneath any footnotes in the footer.

Function Signature

tab_source_note(
  data,
  source_note
)
gtcars |>
  dplyr::select(mfr, model, year, hp, msrp) |>
  dplyr::slice_head(n = 5) |>
  gt() |>
  fmt_currency(columns = msrp, decimals = 0) |>
  cols_label(
    mfr = "Make",
    model = "Model", 
    year = "Year",
    hp = "HP",
    msrp = "Price"
  ) |>
  tab_source_note(
    source_note = "Data from edmunds.com, collected in 2016."
  )
Make Model Year HP Price
Ford GT 2017 647 $447,000
Ferrari 458 Speciale 2015 597 $291,744
Ferrari 458 Spider 2015 562 $263,553
Ferrari 458 Italia 2014 562 $233,509
Ferrari 488 GTB 2016 661 $245,400
Data from edmunds.com, collected in 2016.

Source notes support markdown and HTML formatting:

countrypops |>
  dplyr::filter(country_name == "United States", year >= 2018) |>
  dplyr::select(year, population) |>
  gt() |>
  fmt_number(columns = population, suffixing = TRUE) |>
  tab_header(title = "US Population Growth") |>
  tab_source_note(
    source_note = md("Source: *UN World Population Prospects* (2022 revision)")
  ) |>
  tab_source_note(
    source_note = md("Available at [population.un.org](https://population.un.org)")
  )
US Population Growth
year population
2018 328.53M
2019 330.23M
2020 331.58M
2021 332.10M
2022 334.02M
2023 336.81M
2024 340.11M
Source: UN World Population Prospects (2022 revision)
Available at population.un.org

Multiple tab_source_note() calls add additional source notes, appearing in the order they’re called.

9.7 Customizing footnote marks with opt_footnote_marks()

The default numeric footnote marks (1, 2, 3…) can be changed to letters, symbols, or custom sequences.

Function Signature

opt_footnote_marks(
  data,
  marks = "numbers"
)

9.7.1 Preset mark sequences

Several keywords provide common mark styles:

sza |>
  dplyr::filter(latitude == 20, month == "jan", !is.na(sza)) |>
  dplyr::select(-latitude, -month) |>
  gt() |>
  tab_footnote(
    footnote = "True Solar Time in hours.",
    locations = cells_column_labels(columns = tst)
  ) |>
  tab_footnote(
    footnote = "Solar Zenith Angle in degrees.",
    locations = cells_column_labels(columns = sza)
  ) |>
  opt_footnote_marks(marks = "letters")
tsta szab
0700 84.9
0730 78.7
0800 72.7
0830 66.1
0900 61.5
0930 56.5
1000 52.1
1030 48.3
1100 45.5
1130 43.6
1200 43.0
a True Solar Time in hours.
b Solar Zenith Angle in degrees.

Available keywords:

  • "numbers": 1, 2, 3… (default)
  • "letters": a, b, c…
  • "LETTERS": A, B, C…
  • "standard": *, †, ‡, § (four traditional symbols)
  • "extended": *, †, ‡, §, ‖, ¶ (six symbols)

9.7.2 Using standard symbols

Traditional academic publishing often uses symbols for footnotes:

exibble |>
  dplyr::select(row, char, num) |>
  gt(rowname_col = "row") |>
  tab_footnote(
    footnote = "Character strings of varying length.",
    locations = cells_column_labels(columns = char)
  ) |>
  tab_footnote(
    footnote = "Numeric values with decimal precision.",
    locations = cells_column_labels(columns = num)
  ) |>
  tab_footnote(
    footnote = "Row identifiers for reference.",
    locations = cells_stubhead()
  ) |>
  tab_stubhead(label = "Row") |>
  opt_footnote_marks(marks = "standard")
Row* char num
row_1 apricot 1.111e-01
row_2 banana 2.222e+00
row_3 coconut 3.333e+01
row_4 durian 4.444e+02
row_5 NA 5.550e+03
row_6 fig NA
row_7 grapefruit 7.770e+05
row_8 honeydew 8.880e+06
* Row identifiers for reference.
Character strings of varying length.
Numeric values with decimal precision.

When more footnotes exist than symbols, marks double (**, ††, etc.) then triple, and so on.

9.7.3 Custom mark sequences

You can provide any character vector as custom marks:

towny |>
  dplyr::select(name, population_2021, land_area_km2) |>
  dplyr::slice_head(n = 5) |>
  gt(rowname_col = "name") |>
  fmt_integer(columns = population_2021) |>
  fmt_number(columns = land_area_km2, decimals = 1) |>
  cols_label(
    population_2021 = "Population",
    land_area_km2 = "Land Area"
  ) |>
  tab_footnote(
    footnote = "2021 Canadian Census.",
    locations = cells_column_labels(columns = population_2021)
  ) |>
  tab_footnote(
    footnote = "Square kilometers.",
    locations = cells_column_labels(columns = land_area_km2)
  ) |>
  opt_footnote_marks(marks = c("†", "‡"))
Population Land Area
Addington Highlands 2,534 1,294.0
Adelaide Metcalfe 3,011 331.1
Adjala-Tosorontio 10,989 371.5
Admaston/Bromley 2,995 519.6
Ajax 126,666 66.6
2021 Canadian Census.
Square kilometers.

Unicode symbols, emoji, or any text can serve as custom marks.

9.8 Formatting footnote marks with opt_footnote_spec()

Beyond choosing which marks to use, you can control how they’re formatted (superscript, bold, italicized, or enclosed in brackets).

Function Signature

opt_footnote_spec(
  data,
  spec_ref = NULL,  
  spec_ftr = NULL   
)

The specification string uses control characters:

  • ^: Superscript
  • b: Bold
  • i: Italic
  • (, ): Parentheses
  • [, ]: Square brackets
  • .: Period after mark
  • x: Placeholder for the mark itself (optional)

9.8.1 Customizing mark appearance

The specification string lets you combine multiple formatting options. For example, you might want marks in the table to be superscript and bold for visibility, while marks in the footer use a different style to fit the surrounding text.

illness |>
  dplyr::slice_head(n = 5) |>
  dplyr::select(test, units, day_3, day_4) |>
  gt(rowname_col = "test") |>
  cols_label(
    units = "Units",
    day_3 = "Day 3",
    day_4 = "Day 4"
  ) |>
  fmt_units(columns = units) |>
  tab_footnote(
    footnote = "Standard international units.",
    locations = cells_column_labels(columns = units)
  ) |>
  tab_footnote(
    footnote = "Morning measurement.",
    locations = cells_column_labels(columns = day_3)
  ) |>
  opt_footnote_spec(
    spec_ref = "^bx",
    spec_ftr = "(x)"
  )
Units1 Day 32 Day 4
Viral load copies per mL 12000.00 4200.00
WBC ×109/L 5.26 4.26
Neutrophils ×109/L 4.87 4.72
RBC ×1012/L 5.72 5.98
Hb g/L 153.00 135.00
(1) Standard international units.
(2) Morning measurement.

Here, marks in the table are superscript and bold (^b), while marks in the footer appear in parentheses ((x)).

9.8.2 Academic-style footnotes

Academic journals often have specific requirements for footnote formatting. A common convention uses superscript italic letters in the table body with a period following each mark in the footer.

sp500 |>
  dplyr::filter(date >= "2015-03-01" & date <= "2015-03-05") |>
  dplyr::select(date, open, high, low, close) |>
  gt() |>
  fmt_date(columns = date, date_style = "yMMMd") |>
  fmt_currency(columns = c(open, high, low, close)) |>
  tab_footnote(
    footnote = "New York Stock Exchange opening bell.",
    locations = cells_column_labels(columns = open)
  ) |>
  tab_footnote(
    footnote = "Intraday high.",
    locations = cells_column_labels(columns = high)
  ) |>
  opt_footnote_marks(marks = "letters") |>
  opt_footnote_spec(
    spec_ref = "^i",
    spec_ftr = "x."
  )
date opena highb low close
Mar 5, 2015 $2,098.54 $2,104.25 $2,095.22 $2,101.04
Mar 4, 2015 $2,107.72 $2,107.72 $2,094.49 $2,098.53
Mar 3, 2015 $2,115.76 $2,115.76 $2,098.26 $2,107.78
Mar 2, 2015 $2,105.23 $2,117.52 $2,104.50 $2,117.39
a. New York Stock Exchange opening bell.
b. Intraday high.

Superscript italicized letters in the table, with a trailing period in the footer, a common academic convention.

9.8.3 Bracketed footnotes

Technical documentation and some scientific fields prefer bracketed numbers over superscripts. This style keeps marks at baseline level, which some readers find less disruptive to reading flow.

pizzaplace |>
  dplyr::count(size, name = "orders") |>
  gt(rowname_col = "size") |>
  fmt_integer(columns = orders) |>
  tab_stubhead(label = "Size") |>
  tab_footnote(
    footnote = "S=Small, M=Medium, L=Large, XL=Extra Large, XXL=Double Extra Large",
    locations = cells_stubhead()
  ) |>
  tab_footnote(
    footnote = "Total orders for 2015.",
    locations = cells_column_labels(columns = orders)
  ) |>
  cols_label(orders = "Orders") |>
  opt_footnote_marks(marks = "numbers") |>
  opt_footnote_spec(
    spec_ref = "[x]",
    spec_ftr = "[x]"
  )
Size[1] Orders[2]
L 18,956
M 15,635
S 14,403
XL 552
XXL 28
[1] S=Small, M=Medium, L=Large, XL=Extra Large, XXL=Double Extra Large
[2] Total orders for 2015.

Bracketed numbers appear both in the table and footer (useful for technical documents).

9.9 Controlling footnote order with opt_footnote_order()

When a table has both marked and unmarked footnotes, you can control their relative ordering in the footer.

Function Signature

opt_footnote_order(
  data,
  order = "marks_last"
)

Keywords:

  • "marks_last": Unmarked footnotes appear first, marked footnotes follow (default)
  • "marks_first": Marked footnotes appear first, unmarked follow
  • "preserve_order": Footnotes appear in the order tab_footnote() was called
gtcars |>
  dplyr::select(mfr, model, hp, mpg_c) |>
  dplyr::slice_head(n = 5) |>
  gt() |>
  cols_label(
    mfr = "Make",
    model = "Model",
    hp = "HP",
    mpg_c = "MPG"
  ) |>
  tab_footnote(footnote = "All specifications from manufacturer.") |>
  tab_footnote(
    footnote = "Brake horsepower.",
    locations = cells_column_labels(columns = hp)
  ) |>
  tab_footnote(footnote = "EPA city driving estimate.") |>
  tab_footnote(
    footnote = "City fuel economy.",
    locations = cells_column_labels(columns = mpg_c)
  ) |>
  opt_footnote_order(order = "marks_first")
Make Model HP1 MPG2
Ford GT 647 11
Ferrari 458 Speciale 597 13
Ferrari 458 Spider 562 13
Ferrari 458 Italia 562 13
Ferrari 488 GTB 661 15
1 Brake horsepower.
2 City fuel economy.
All specifications from manufacturer.
EPA city driving estimate.

With "marks_first", the two marked footnotes (for HP and MPG) appear before the two unmarked footnotes.

9.12 Practical examples

The techniques covered in this chapter come together in real-world tables where footnotes serve multiple purposes simultaneously. The following examples demonstrate how to combine location targeting, mark customization, and footer styling to create polished, professional tables.

9.12.1 Comprehensive financial table

Financial reports often require extensive annotation: definitions for technical terms, clarifications for unusual values, and source citations for data provenance. This example shows how multiple footnote types work together in a single table.

sp500 |>
  dplyr::filter(date >= "2015-01-05" & date <= "2015-01-09") |>
  dplyr::select(date, open, high, low, close, volume) |>
  dplyr::mutate(
    change = close - open,
    pct_change = (close - open) / open
  ) |>
  gt() |>
  tab_header(
    title = "S&P 500 Daily Performance",
    subtitle = "First trading week of 2015"
  ) |>
  fmt_date(columns = date, date_style = "yMMMd") |>
  fmt_currency(columns = c(open, high, low, close, change)) |>
  fmt_percent(columns = pct_change, decimals = 2) |>
  fmt_number(columns = volume, suffixing = TRUE) |>
  cols_label(
    date = "Date",
    open = "Open",
    high = "High",
    low = "Low", 
    close = "Close",
    volume = "Volume",
    change = "Change",
    pct_change = "% Change"
  ) |>
  tab_footnote(
    footnote = "New York Stock Exchange opening price.",
    locations = cells_column_labels(columns = open)
  ) |>
  tab_footnote(
    footnote = "Intraday trading range.",
    locations = cells_column_labels(columns = c(high, low))
  ) |>
  tab_footnote(
    footnote = "Shares traded (approximate).",
    locations = cells_column_labels(columns = volume)
  ) |>
  tab_footnote(
    footnote = "Worst single-day performance of the week.",
    locations = cells_body(columns = pct_change, rows = pct_change == min(pct_change))
  ) |>
  tab_footnote(footnote = "All values in USD.") |>
  tab_source_note(
    source_note = md("Data: *Yahoo Finance* via the **gt** package")
  ) |>
  opt_footnote_marks(marks = "standard") |>
  opt_footnote_spec(spec_ref = "^", spec_ftr = "x —") |>
  tab_options(footnotes.multiline = FALSE)
S&P 500 Daily Performance
First trading week of 2015
Date Open* High Low Close Volume Change % Change
Jan 9, 2015 $2,063.45 $2,064.43 $2,038.33 $2,044.81 3.36B −$18.64 −0.90%
Jan 8, 2015 $2,030.61 $2,064.08 $2,030.61 $2,062.14 3.93B $31.53 1.55%
Jan 7, 2015 $2,005.55 $2,029.61 $2,005.55 $2,025.90 3.81B $20.35 1.01%
Jan 6, 2015 $2,022.15 $2,030.25 $1,992.44 $2,002.61 4.46B −$19.54 −0.97%
Jan 5, 2015 $2,054.44 $2,054.44 $2,017.34 $2,020.58 3.80B −$33.86 § −1.65%
All values in USD. * New York Stock Exchange opening price. Intraday trading range. Shares traded (approximate). § Worst single-day performance of the week.
Data: Yahoo Finance via the gt package

This financial table demonstrates multiple footnote targets, both marked and unmarked footnotes, customized marks, and styled footer elements.

9.12.2 Research table with citations

Scientific and clinical publications have their own footnote conventions, often favoring letters or symbols over numbers and requiring precise citation formatting. This example shows a clinical laboratory report with footnotes on column spanners, stub cells, and a properly formatted source citation.

illness |>
  dplyr::slice_head(n = 6) |>
  dplyr::select(test, units, norm_l, norm_u, day_3, day_7) |>
  gt(rowname_col = "test") |>
  tab_header(
    title = "Clinical Laboratory Results",
    subtitle = "Seven-day patient monitoring"
  ) |>
  tab_spanner(
    label = "Reference Range",
    columns = c(norm_l, norm_u)
  ) |>
  tab_spanner(
    label = "Patient Values",
    columns = c(day_3, day_7)
  ) |>
  cols_label(
    units = "Units",
    norm_l = "Lower",
    norm_u = "Upper",
    day_3 = "Admission",
    day_7 = "Discharge"
  ) |>
  tab_footnote(
    footnote = "Reference ranges from clinical guidelines.",
    locations = cells_column_spanners(spanners = "Reference Range")
  ) |>
  tab_footnote(
    footnote = "Day 3 = admission; Day 7 = planned discharge.",
    locations = cells_column_spanners(spanners = "Patient Values")
  ) |>
  tab_footnote(
    footnote = md("Hb = Hemoglobin; PLT = Platelet count"),
    locations = cells_stub(rows = c("Hb", "PLT"))
  ) |>
  tab_source_note(
    source_note = md("Laboratory procedures per *Clinical Chemistry* standards (2023)")
  ) |>
  opt_footnote_marks(marks = "letters") |>
  opt_footnote_spec(spec_ref = "^i", spec_ftr = "^x^")
Clinical Laboratory Results
Seven-day patient monitoring
Units
Reference Rangea
Patient Valuesb
Lower Upper Admission Discharge
Viral load copies per mL NA NA 12000.00 760.00
WBC x10^9 / L 4 10.0 5.26 24.77
Neutrophils x10^9 / L 2 8.0 4.87 22.08
RBC x10^12 / L 4 5.5 5.72 4.12
Hbc g / L 120 160.0 153.00 75.00
PLTc x10^9 / L 100 300.0 67.00 74.10
a Reference ranges from clinical guidelines.
b Day 3 = admission; Day 7 = planned discharge.
c Hb = Hemoglobin; PLT = Platelet count
Laboratory procedures per Clinical Chemistry standards (2023)

This clinical research table uses superscript italic marks, attaches footnotes to spanners and stub cells, and includes a properly cited source note.

Footnotes transform tables from bare data presentations into documented, contextualized reports. Whether explaining methodology, defining terms, citing sources, or highlighting anomalies, the footer section provides structured space for the supplementary information that readers need. With gt’s flexible location helpers and extensive customization options, you can craft footnotes that integrate seamlessly with any table design.

9.13 Summary

This chapter has covered the footer section of gt tables, where footnotes and source notes provide supplementary information without cluttering the main presentation.

The key concepts we’ve explored:

  • footnotes link explanatory text to specific table locations via marks. The tab_footnote() function attaches notes to cells, columns, rows, spanners, or any other table element using the cells_*() location helpers.
  • source notes provide general context (typically data citations) without marks. Multiple tab_source_note() calls add notes in sequence, and both md() and html() helpers enable rich formatting.
  • footnote marks can be customized with opt_footnote_marks() (choosing numbers, letters, standard symbols, or extended symbols) and opt_footnote_spec() (controlling superscript, parentheses, and other formatting).
  • location targeting uses the same cells_*() functions found throughout gt: cells_body(), cells_column_labels(), cells_stub(), cells_column_spanners(), cells_row_groups(), cells_summary(), and more.
  • mark placement can be controlled with the placement argument, positioning marks to the left or right of cell content.

Footnotes serve multiple purposes: defining abbreviations, explaining methodology, noting data limitations, citing sources, and highlighting exceptions. The key is using them well. Too many footnotes can overwhelm a table, while strategic footnotes enhance understanding.

The next chapter introduces nanoplots: tiny embedded visualizations that add a visual dimension to tabular data. You’ll learn to create line plots, bar plots, and box plots that fit within table cells, revealing trends and distributions at a glance.