4  Formatting dates, text, and special values

Numbers may dominate most datasets, but tables come alive when they incorporate the full spectrum of data types. Dates tell us when something happened. Text provides names, descriptions, and categorical labels. URLs connect tables to the broader web. Images and icons add visual meaning that words and numbers alone cannot convey. Flags identify countries at a glance. Chemical formulas render with proper subscripts. This chapter explores the formatting functions that handle all of these non-numeric data types, transforming raw values into polished presentations.

The formatters covered here share a common philosophy with the numeric functions from the previous chapter: they operate on the underlying data without modifying it, they support locale-aware rendering where appropriate, and they integrate seamlessly into gt pipelines. But they also introduce capabilities unique to their data types. Date formatters must navigate the labyrinth of international conventions (is January 5th written as 1/5 or 5/1?). Text formatters can render Markdown, generate hyperlinks, or display email addresses. Image formatters embed graphics directly into cells. And special formatters like fmt_flag() and fmt_icon() bridge the gap between data and visual communication.

Consider a table of international sales data. The numeric columns benefit from the fmt_*() functions we’ve already learned. But the country codes: they’re more meaningful as flag icons. Report dates need to follow the conventions of whoever is reading the table. And those product descriptions with markdown formatting should of course render properly rather than showing raw asterisks and brackets. This chapter gives you the tools to handle all such cases.

We’ll start with temporal data (dates, times, and durations) where the challenge lies in choosing among dozens of valid formats for the same underlying value. From there, we move to text and URL formatting, including Markdown rendering. Then we explore the visual formatters that can embed images, country flags, and icons directly into your table cells. Finally, we cover specialized formatters for units, chemical formulas, and country names. By the end of this chapter, you’ll have the complete toolkit for formatting any type of data that might appear in your tables.

4.1 Date, time, and duration formats

Temporal data presents unique formatting challenges. The same date can be expressed in dozens of valid formats, and the choice depends on audience, locale, and context. gt provides fmt_date(), fmt_time(), fmt_datetime(), and fmt_duration() to handle these cases with support for 41 preset date styles and extensive localization.

4.1.1 fmt_date()

The fmt_date() function formats date values using one of 41 preset styles. Input can be Date objects, POSIXt datetime objects, or character strings in ISO 8601 format.

Here is the function’s signature:

fmt_date(
  data,
  columns = everything(),
  rows = everything(),
  date_style = "iso",
  pattern = "{x}",
  locale = NULL
)

Let’s format dates from the exibble dataset in “Month Day, Year” style:

exibble |>
  dplyr::select(date, time) |>
  gt() |>
  fmt_date(
    columns = date,
    date_style = "month_day_year"
  )
date time
January 15, 2015 13:35
February 15, 2015 14:40
March 15, 2015 15:45
April 15, 2015 16:50
May 15, 2015 17:55
June 15, 2015 NA
NA 19:10
August 15, 2015 20:20

The date column now displays in “Month Day, Year” format, a common style in American English contexts.

Different date styles serve different purposes:

exibble |>
  dplyr::select(date) |>
  dplyr::slice(1:4) |>
  gt() |>
  cols_add(
    iso = date,
    full = date,
    compact = date
  ) |>
  fmt_date(columns = date, date_style = "wday_month_day_year") |>
  fmt_date(columns = iso, date_style = "iso") |>
  fmt_date(columns = full, date_style = "day_month_year") |>
  fmt_date(columns = compact, date_style = "yMd") |>
  cols_label(
    date = "Full (US)",
    iso = "ISO 8601",
    full = "Full (UK)",
    compact = "Compact"
  )
Full (US) ISO 8601 Full (UK) Compact
Thursday, January 15, 2015 2015-01-15 15 January 2015 1/15/2015
Sunday, February 15, 2015 2015-02-15 15 February 2015 2/15/2015
Sunday, March 15, 2015 2015-03-15 15 March 2015 3/15/2015
Wednesday, April 15, 2015 2015-04-15 15 April 2015 4/15/2015

This table demonstrates four different date styles from the same source data. The “flexible” styles (like "yMd") automatically adapt to the specified locale.

To explore all 41 available date styles, use info_date_style(), which displays an informative gt table showing each style’s name, a description, and whether it’s locale-flexible:

info_date_style()

For reports that combine data from different regional sources, you can use from_column() to apply different date styles per row:

dplyr::tibble(
  region = c("United States", "United Kingdom", "Japan", "Germany"),
  report_date = as.Date(c("2025-03-15", "2025-03-15", "2025-03-15", "2025-03-15")),
  date_format = c("month_day_year", "day_month_year", "yMd", "day_month_year")
) |>
  gt() |>
  fmt_date(
    columns = report_date,
    date_style = from_column("date_format")
  ) |>
  cols_hide(columns = date_format) |>
  cols_label(
    region = "Region",
    report_date = "Report Date"
  )
Region Report Date
United States March 15, 2025
United Kingdom 15 March 2025
Japan 3/15/2025
Germany 15 March 2025

Each row displays the date in the format conventional for that region: “March 15, 2025” for the US, “15 March 2025” for the UK, and so on. This technique is valuable when creating localized reports or when displaying historical data that was originally recorded in different regional formats.

You can also use from_column() to switch locales, which affects how month and day names are rendered. When combined with flexible date styles (like "yMMMd" or "yMMMEd"), locales also change the ordering of date components to match regional conventions:

dplyr::tibble(
  country = c("United States", "France", "Germany", "Japan", "China"),
  event_date = as.Date("2025-06-15"),
  locale_code = c("en_US", "fr", "de", "ja", "zh")
) |>
  gt() |>
  fmt_date(
    columns = event_date,
    date_style = "yMMMd",
    locale = from_column("locale_code")
  ) |>
  cols_hide(columns = locale_code) |>
  cols_label(
    country = "Country",
    event_date = "Event Date"
  )
Country Event Date
United States Jun 15, 2025
France 15 juin 2025
Germany 15. Juni 2025
Japan 2025年6月15日
China 2025年6月15日

Notice that it’s not just translation: the US format puts the month before the day ("Jun 15, 2025"), while European locales put the day first ("15 juin 2025" in French). Asian locales typically use year-month-day order. The flexible date styles automatically adapt to each locale’s conventions for both component ordering and separators.

4.1.2 fmt_time()

Time formatting follows similar principles. The fmt_time() function formats input values to time values using one of 25 preset time styles. Input can be in the form of POSIXt (i.e., datetimes), character (must be in the ISO 8601 forms of HH:MM:SS or YYYY-MM-DD HH:MM:SS), or Date (which always results in the formatting of 00:00:00).

Here is the function’s signature:

fmt_time(
  data,
  columns = everything(),
  rows = everything(),
  time_style = "iso",
  pattern = "{x}",
  locale = NULL
)

Let’s format times from the exibble dataset in 12-hour format:

exibble |>
  dplyr::select(time) |>
  gt() |>
  fmt_time(
    columns = time,
    time_style = "h_m_p"
  )
time
1:35 PM
2:40 PM
3:45 PM
4:50 PM
5:55 PM
NA
7:10 PM
8:20 PM

The times now display in 12-hour format with AM/PM indicators, a common format for general audiences.

To see all available time styles, use info_time_style():

info_time_style()

Like date styles, some time styles are locale-flexible (adapting to 12-hour or 24-hour conventions based on the locale), while others produce fixed output regardless of locale.

4.1.3 fmt_datetime()

When you have full datetime values, fmt_datetime() combines date and time formatting. This function offers two approaches: using preset styles for the date and time components separately, or using a custom format string for complete control over the output.

Here is the function’s signature:

fmt_datetime(
  data,
  columns = everything(),
  rows = everything(),
  date_style = "iso",
  time_style = "iso",
  sep = " ",
  format = NULL,
  tz = NULL,
  pattern = "{x}",
  locale = NULL
)

The simplest approach combines preset date_style and time_style values:

exibble |>
  dplyr::select(datetime) |>
  gt() |>
  fmt_datetime(
    columns = datetime,
    date_style = "yMMMd",
    time_style = "Hm"
  )
datetime
Jan 1, 2018 02:22
Feb 2, 2018 14:33
Mar 3, 2018 03:44
Apr 4, 2018 15:55
May 5, 2018 04:00
Jun 6, 2018 16:11
Jul 7, 2018 05:22
NA

The datetime column now shows both the date (in “Feb 29, 2000” style) and time (in 24-hour format), providing complete temporal information. The sep argument controls the separator between date and time (defaults to a single space).

4.1.3.1 Custom formatting with the format argument

For complete control over datetime output, the format argument accepts formatting strings in two different syntaxes: CLDR (Common Locale Data Repository) datetime patterns and strptime format codes. Both are powerful, but CLDR patterns offer better locale support and more formatting options.

CLDR datetime patterns use pattern characters that repeat to indicate output width. Here are some examples using a datetime of "2018-07-04T22:05:09":

  • "EEEE, MMMM d, y""Wednesday, July 4, 2018"
  • "MMM d, y 'at' h:mm a""Jul 4, 2018 at 10:05 PM"
  • "y-MM-dd HH:mm""2018-07-04 22:05"

The key CLDR pattern characters include:

Character Meaning Examples
y Year y → “2018”, yy → “18”
M Month M → “7”, MM → “07”, MMM → “Jul”, MMMM → “July”
d Day of month d → “4”, dd → “04”
E Day of week E → “Wed”, EEEE → “Wednesday”
H Hour (0-23) H → “22”, HH → “22”
h Hour (1-12) h → “10”, hh → “10”
m Minute m → “5”, mm → “05”
s Second s → “9”, ss → “09”
a AM/PM a → “PM”

Literal text can be included by surrounding it with single quotes:

exibble |>
  dplyr::select(datetime) |>
  dplyr::slice(1:4) |>
  gt() |>
  fmt_datetime(
    columns = datetime,
    format = "EEEE, MMMM d, y 'at' h:mm a"
  )
datetime
Monday, January 1, 2018 at 2:22 AM
Friday, February 2, 2018 at 2:33 PM
Saturday, March 3, 2018 at 3:44 AM
Wednesday, April 4, 2018 at 3:55 PM

strptime format codes use a % prefix for each component. The same datetime formatted with strptime codes:

  • "%A, %B %e, %Y""Wednesday, July 4, 2018"
  • "%b %e, %Y at %I:%M %p""Jul 4, 2018 at 10:05 PM"
  • "%Y-%m-%d %H:%M""2018-07-04 22:05"

Common strptime codes include:

Code Meaning Example
%Y 4-digit year "2018"
%y 2-digit year "18"
%m Month number (zero-padded) "07"
%b Abbreviated month name "Jul"
%B Full month name "July"
%d Day (zero-padded) "04"
%e Day (space-padded) " 4"
%A Full weekday name "Wednesday"
%H Hour 0-23 "22"
%I Hour 1-12 "10"
%M Minute "05"
%S Second "09"
%p AM/PM "PM"

Here’s the same friendly datetime format from before, now using strptime codes instead of CLDR patterns:

exibble |>
  dplyr::select(datetime) |>
  dplyr::slice(1:4) |>
  gt() |>
  fmt_datetime(
    columns = datetime,
    format = "%A, %B %e, %Y at %I:%M %p"
  )
datetime
Monday, January 1, 2018 at 02:22 AM
Friday, February 2, 2018 at 02:33 PM
Saturday, March 3, 2018 at 03:44 AM
Wednesday, April 4, 2018 at 03:55 PM

The output is nearly identical to the CLDR version. The "%A" gives the full weekday name, "%B" the full month name, "%e" the day without zero-padding, and "%I:%M %p" produces 12-hour time with AM/PM.

4.1.3.2 Working with time zones

The tz argument lets you convert datetimes to a specific time zone for display. This is particularly useful when your data is stored in UTC but you want to display it in local time:

exibble |>
  dplyr::select(datetime) |>
  dplyr::slice(1:3) |>
  gt() |>
  fmt_datetime(
    columns = datetime,
    format = "EEEE, MMMM d, y 'at' h:mm a (zzzz)",
    tz = "America/Vancouver"
  )
datetime
Monday, January 1, 2018 at 2:22 AM (Pacific Standard Time)
Friday, February 2, 2018 at 2:33 PM (Pacific Standard Time)
Saturday, March 3, 2018 at 3:44 AM (Pacific Standard Time)

The "zzzz" pattern character displays the full time zone name. You can use shorter variants like "z" (abbreviated) or "Z" (UTC offset).

4.1.3.3 CLDR vs strptime: which to choose?

CLDR patterns are generally preferred because they have:

  1. better locale support, where patterns adapt to locale conventions automatically
  2. more options: patterns for eras, quarters, flexible day periods (“in the afternoon”), and more
  3. richer time zone display options

Use strptime when you need compatibility with R’s base date formatting functions or when working with existing format strings from other code.

For a comprehensive reference of all CLDR pattern fields and strptime format codes, see Appendix B.

4.1.4 fmt_duration()

For time intervals and durations, fmt_duration() converts numeric values (representing seconds or other units) into human-readable duration strings.

Here is the function’s signature:

fmt_duration(
  data,
  columns = everything(),
  rows = everything(),
  input_units = NULL,
  output_units = NULL,
  duration_style = c("narrow", "wide", "colon-sep", "iso"),
  trim_zero_units = TRUE,
  max_output_units = NULL,
  pattern = "{x}",
  locale = NULL
)

Let’s format task durations from raw second values:

dplyr::tibble(
  task = c("Backup", "Index rebuild", "Report generation"),
  duration_secs = c(45, 3661, 127)
) |>
  gt() |>
  fmt_duration(
    columns = duration_secs,
    input_units = "seconds"
  )
task duration_secs
Backup 45s
Index rebuild 1h 1m 1s
Report generation 2m 7s

The raw second counts are transformed into readable durations like “1h 1m 1s”, making the time requirements immediately clear.

The duration_style argument controls the output format:

dplyr::tibble(
  task = c("Short task", "Medium task", "Long task"),
  seconds = c(90, 3725, 86520)
) |>
  gt() |>
  cols_add(
    narrow = seconds,
    wide = seconds,
    colon = seconds,
    iso = seconds
  ) |>
  fmt_duration(columns = narrow, input_units = "seconds", duration_style = "narrow") |>
  fmt_duration(columns = wide, input_units = "seconds", duration_style = "wide") |>
  fmt_duration(columns = colon, input_units = "seconds", duration_style = "colon-sep") |>
  fmt_duration(columns = iso, input_units = "seconds", duration_style = "iso") |>
  cols_hide(columns = seconds) |>
  cols_label(
    task = "Task",
    narrow = "Narrow",
    wide = "Wide",
    colon = "Colon-sep",
    iso = "ISO"
  )
Task Narrow Wide Colon-sep ISO
Short task 1m 30s 1 minute 30 seconds 00:01:30 P1M30S
Medium task 1h 2m 5s 1 hour 2 minutes 5 seconds 01:02:05 P1H2M5S
Long task 1d 2m 1 day 2 minutes 1/00:02:00 P1DT0H2M

Each style serves different purposes: "narrow" is compact, "wide" is more readable, "colon-sep" follows familiar clock notation (HH:MM:SS), and "iso" produces ISO 8601 duration format.

4.3 Flags and country formatting

When presenting international data, flags and country names can provide immediate visual recognition and context.

4.3.1 fmt_flag()

The fmt_flag() function converts 2- or 3-letter ISO 3166-1 country codes into circular flag icons. The function seamlessly handles both uppercase and lowercase codes.

Here is the function’s signature:

fmt_flag(
  data,
  columns = everything(),
  rows = everything(),
  height = "1em",
  sep = " ",
  use_title = TRUE,
  locale = NULL
)
countrypops |>
  dplyr::filter(year == 2021) |>
  dplyr::slice_max(population, n = 8) |>
  dplyr::select(country_code_2, country_name, population) |>
  gt() |>
  fmt_flag(columns = country_code_2) |>
  fmt_integer(columns = population) |>
  cols_label(
    country_code_2 = "",
    country_name = "Country",
    population = "Population (2021)"
  )
Country Population (2021)
India 1,414,203,896
China 1,412,360,000
United States 332,099,760
Indonesia 276,758,053
Pakistan 239,477,801
Nigeria 218,529,286
Brazil 209,550,294
Bangladesh 167,658,854

The two-letter country codes are replaced with their corresponding flag icons. Hovering over a flag shows the country name as a tooltip (controlled by use_title = TRUE, the default). This adds visual interest and makes it easier to quickly identify countries.

4.3.1.1 Controlling flag size

The height argument adjusts the size of flag icons. The default "1em" scales with the text size, but you can specify other CSS units:

dplyr::tibble(
  country = c("US", "CN", "IN", "BR"),
  small = country,
  medium = country,
  large = country
) |>
  gt() |>
  fmt_flag(columns = small, height = "0.8em") |>
  fmt_flag(columns = medium, height = "1.5em") |>
  fmt_flag(columns = large, height = "2.5em") |>
  cols_label(
    country = "Code",
    small = "Small",
    medium = "Medium",
    large = "Large"
  ) |>
  cols_align(align = "center", columns = c(small, medium, large))
Code Small Medium Large
US
CN
IN
BR

Different flag sizes can help establish visual hierarchy. Larger flags might be appropriate in header sections or key summary rows, while smaller flags work well in dense data tables.

4.3.1.2 Multiple flags per cell

The fmt_flag() function works well even when there are multiple country codes within the same cell. It parses comma-separated codes automatically, and when rendered to HTML, hovering over each flag icon displays a tooltip with the country name. The input must use commas as delimiters (with no spaces), while the sep argument controls the spacing between rendered flag icons in the output.

dplyr::tibble(
  region = c("North America", "European Union (founders)", "BRICS", "Nordic Countries"),
  countries = c("US,CA,MX", "BE,FR,DE,IT,LU,NL", "BR,RU,IN,CN,ZA", "DK,FI,IS,NO,SE")
) |>
  gt() |>
  fmt_flag(columns = countries, sep = " ") |>
  cols_label(
    region = "Region/Group",
    countries = "Member Countries"
  ) |>
  cols_width(countries ~ px(300))
Region/Group Member Countries
North America
European Union (founders)
BRICS
Nordic Countries

Multiple flags are displayed inline, making it easy to see country groupings or collaborations at a glance. The default sep = " " provides a single space between flags, but you can adjust this for tighter or looser spacing.

Here’s another example that groups countries by population size, demonstrating how fmt_flag() handles dynamically aggregated country codes:

countrypops |>
  dplyr::filter(year == 2021, population < 100000) |>
  dplyr::select(country_code_2, population) |>
  dplyr::mutate(population_class = cut(
    population,
    breaks = scales::breaks_pretty(n = 5)(population)
    )
  ) |>
  dplyr::group_by(population_class) |>
  dplyr::summarize(
    countries = paste0(country_code_2, collapse = ",")
  ) |>
  dplyr::arrange(desc(population_class)) |>
  gt() |>
  tab_header(title = "Countries with Small Populations") |>
  fmt_flag(columns = countries) |>
  fmt_bins(
    columns = population_class,
    fmt = ~ fmt_integer(., suffixing = TRUE)
  ) |>
  cols_label(
    population_class = "Population Range",
    countries = "Countries"
  ) |>
  cols_width(population_class ~ px(150))
Countries with Small Populations
Population Range Countries
80K–100K
60K–80K
40K–60K
20K–40K
0–20K

This example uses paste0() with collapse = "," to aggregate country codes within each population bin, and fmt_flag() seamlessly renders all flags in each cell.

4.3.1.3 Localized tooltips

The locale argument controls the language used in the hover tooltips. This is especially useful when creating tables for international audiences:

dplyr::tibble(
  code = c("JP", "KR", "CN", "TH"),
  flag_en = code,
  flag_ja = code,
  flag_ko = code
) |>
  gt() |>
  fmt_flag(columns = flag_en, locale = "en") |>
  fmt_flag(columns = flag_ja, locale = "ja") |>
  fmt_flag(columns = flag_ko, locale = "ko") |>
  cols_label(
    code = "Code",
    flag_en = "English Tooltip",
    flag_ja = "Japanese Tooltip",
    flag_ko = "Korean Tooltip"
  ) |>
  cols_align(align = "center", columns = c(flag_en, flag_ja, flag_ko))
Code English Tooltip Japanese Tooltip Korean Tooltip
JP
KR
CN
TH

When hovering over these flags, the country names appear in the specified language. The English column shows “Japan”, the Japanese column shows “日本”, and the Korean column shows “일본”.

4.3.1.4 Merging flags into row labels

Flag icons can be merged into the stub column to create visually distinctive row labels:

countrypops |>
  dplyr::filter(country_code_2 %in% c("BE", "NL", "LU")) |>
  dplyr::filter(year %% 10 == 0, year >= 2000) |>
  dplyr::select(country_name, country_code_2, year, population) |>
  tidyr::pivot_wider(names_from = year, values_from = population) |>
  dplyr::arrange(country_name) |>
  gt(rowname_col = "country_name") |>
  fmt_flag(columns = country_code_2) |>
  cols_merge(
    columns = c(country_name, country_code_2),
    pattern = "{2} {1}"
  ) |>
  fmt_integer() |>
  tab_header(title = "Benelux Population by Decade") |>
  tab_spanner(columns = everything(), label = "Year")
Benelux Population by Decade
Year
2000 2010 2020
Belgium 10,251,250 10,895,586 11,538,604
Luxembourg 436,300 506,953 630,419
Netherlands 15,925,513 16,615,394 17,441,500

The stub now displays flag icons followed by country names, creating an elegant visual identifier for each row. This technique is particularly effective for country comparisons or regional analyses.

4.3.2 fmt_country()

Conversely, if you have country codes but want to display full country names, use fmt_country(). This function accepts both 2-letter and 3-letter ISO 3166-1 country codes and converts them to full country names from the Unicode CLDR (Common Locale Data Repository).

Here is the function’s signature:

fmt_country(
  data,
  columns = everything(),
  rows = everything(),
  pattern = "{x}",
  sep = " ",
  locale = NULL
)
countrypops |>
  dplyr::filter(year == 2021) |>
  dplyr::slice_max(population, n = 5) |>
  dplyr::select(country_code_3, population) |>
  gt() |>
  fmt_country(columns = country_code_3) |>
  fmt_integer(columns = population) |>
  cols_label(
    country_code_3 = "Country",
    population = "Population (2021)"
  )
Country Population (2021)
India 1,414,203,896
China 1,412,360,000
United States 332,099,760
Indonesia 276,758,053
Pakistan 239,477,801

The three-letter country codes are replaced with full country names.

The country names come from the Unicode CLDR (Common Locale Data Repository), where names are agreed upon by consensus. Furthermore, these names can be localized to any of 574 different locales via the locale argument.

Here’s an example showing how the same country code resolves to different exonyms depending on the locale:

dplyr::tibble(
  locale = c("en", "fr", "de", "it", "ja", "zh", "ko", "ar"),
  country = "DE"
) |>
  gt() |>
  fmt_country(columns = country, locale = from_column("locale")) |>
  cols_label(
    locale = "Locale",
    country = "Country Name"
  )
Locale Country Name
en Germany
fr Allemagne
de Deutschland
it Germania
ja ドイツ
zh 德国
ko 독일
ar

ألمانيا

Each row shows “DE” (Germany) translated into the language specified by the locale column: English, French, German, Japanese, Chinese, Arabic, and Russian.

Here’s a more comprehensive example using the films dataset. The fmt_country() function handles multiple comma-separated country codes per cell, which is common for international co-productions:

films |>
  dplyr::filter(year == 1959) |>
  dplyr::select(
    title, run_time, director, countries_of_origin, imdb_url
  ) |>
  gt() |>
  tab_header(title = "Feature Films in Competition at the 1959 Festival") |>
  fmt_country(columns = countries_of_origin, sep = ", ") |>
  fmt_url(
    columns = imdb_url,
    label = fontawesome::fa("imdb", fill = "black")
  ) |>
  cols_merge(
    columns = c(title, imdb_url),
    pattern = "{1} {2}"
  ) |>
  cols_label(
    title = "Film",
    run_time = "Length",
    director = "Director",
    countries_of_origin = "Country"
  ) |>
  opt_vertical_padding(scale = 0.5) |>
  opt_horizontal_padding(scale = 2.5) |>
  opt_table_font(stack = "classical-humanist", weight = "bold") |>
  opt_stylize(style = 1, color = "gray") |>
  tab_options(heading.title.font.size = px(26))
Feature Films in Competition at the 1959 Festival
Film Length Director Country
Araya 1h 30m Margot Benacerraf Venezuela, France
Compulsion 1h 43m Richard Fleischer United States
Eva 1h 32m Rolf Thiele Austria
Fanfare 1h 26m Bert Haanstra Netherlands
Miss April 1h 38m Göran Gentele Sweden
Arms and the Man 1h 40m Franz Peter Wirth Germany
Hiroshima mon amour 1h 30m Alain Resnais France, Japan
Court Martial 1h 24m Kurt Meisel Germany
The Soldiers of Pancho Villa 1h 37m Ismael Rodríguez Mexico
Lajwanti 2h Narendra Suri India
The 400 Blows 1h 39m François Truffaut France
Honeymoon 1h 49m Michael Powell United Kingdom, Spain
Bloody Twilight 1h 28m Andreas Labrinos Greece
Middle of the Night 1h 58m Delbert Mann United States
Nazarín 1h 34m Luis Buñuel Mexico
Black Orpheus 1h 40m Marcel Camus Brazil, France, Italy
A Home for Tanya 1h 40m Lev Kulidzhanov USSR
Policarpo 1h 44m Mario Soldati Italy, France, Spain
Portuguese Rhapsody 1h 26m João Mendes Portugal
Room at the Top 1h 57m Jack Clayton United Kingdom
A Midsummer Night's Dream 1h 16m Jirí Trnka Czechoslovakia
The Snowy Heron 1h 37m Teinosuke Kinugasa Japan
Stars 1h 31m Konrad Wolf East Germany, Bulgaria
The Sinner 1h 30m Shen Tien Taiwan
The Diary of Anne Frank 3h George Stevens United States
Desire 1h 35m Vojtech Jasný Czechoslovakia
Train Without a Timetable 2h 1m Veljko Bulajic Yugoslavia
Sugar Harvest 1h 17m Lucas Demare Argentina
Édes Anna 1h 24m Zoltán Fábri Hungary

Notice that fmt_country() can resolve historical country codes that no longer exist. Codes like "SU" (USSR), "CS" (Czechoslovakia), and "YU" (Yugoslavia) are properly resolved to their historical names, which is essential when working with archival data.

4.3.2.1 Localized country names

The locale argument enables country names to be displayed in different languages, making tables more accessible to international audiences:

dplyr::tibble(
  code = c("JP", "DE", "BR", "IN", "ZA"),
  english = code,
  japanese = code,
  german = code
) |>
  gt() |>
  fmt_country(columns = english, locale = "en") |>
  fmt_country(columns = japanese, locale = "ja") |>
  fmt_country(columns = german, locale = "de") |>
  cols_label(
    code = "Code",
    english = "English",
    japanese = "日本語",
    german = "Deutsch"
  )
Code English 日本語 Deutsch
JP Japan 日本 Japan
DE Germany ドイツ Deutschland
BR Brazil ブラジル Brasilien
IN India インド Indien
ZA South Africa 南アフリカ Südafrika

The same country codes are rendered in English, Japanese, and German. This is particularly useful for creating multilingual reports or tables intended for specific regional audiences.

4.3.2.2 Multiple countries per cell

When cells contain multiple comma-separated country codes, fmt_country() handles them automatically. The sep argument controls the separator between country names:

dplyr::tibble(
  film = c("The Grand Budapest Hotel", "Amélie", "Run Lola Run"),
  countries = c("US,DE", "FR", "DE")
) |>
  gt() |>
  fmt_country(columns = countries, sep = " / ") |>
  cols_label(
    film = "Film",
    countries = "Production Countries"
  )
Film Production Countries
The Grand Budapest Hotel United States / Germany
Amélie France
Run Lola Run Germany

For films with co-production arrangements, multiple countries are displayed with a custom separator (here, ” / “), making the relationships clear while maintaining readability.

4.3.2.3 Combining fmt_flag() and fmt_country()

Flag icons and country names work beautifully together. Here’s how to combine them using cols_merge():

countrypops |>
  dplyr::filter(year == 2021) |>
  dplyr::slice_max(population, n = 8) |>
  dplyr::select(country_code_2, country_code_3, population) |>
  gt() |>
  fmt_flag(columns = country_code_2) |>
  fmt_country(columns = country_code_3) |>
  cols_merge(
    columns = c(country_code_2, country_code_3),
    pattern = "{1} {2}"
  ) |>
  fmt_integer(columns = population) |>
  cols_label(
    country_code_2 = "Country",
    population = "Population (2021)"
  )
Country Population (2021)
India 1,414,203,896
China 1,412,360,000
United States 332,099,760
Indonesia 276,758,053
Pakistan 239,477,801
Nigeria 218,529,286
Brazil 209,550,294
Bangladesh 167,658,854

The merged column now displays both the flag icon and the country name side by side, creating a visually rich and informative presentation. The flag provides instant visual recognition while the name ensures clarity.

4.4 Icons in table cells

4.4.1 fmt_icon()

For adding Font Awesome icons to table cells, fmt_icon() converts icon names to rendered icons.

Here is the function’s signature:

fmt_icon(
  data,
  columns = everything(),
  rows = everything(),
  height = "1em",
  sep = " ",
  stroke_color = NULL,
  stroke_width = NULL,
  stroke_alpha = NULL,
  fill_color = NULL,
  fill_alpha = NULL,
  margin_left = NULL,
  margin_right = NULL,
  v_adjust = NULL,
  a11y = c("semantic", "decorative", "none")
)

Let’s add Font Awesome icons to represent different file types:

dplyr::tibble(
  category = c("Documents", "Images", "Music", "Videos"),
  icon = c("file-alt", "image", "music", "video"),
  count = c(42, 18, 95, 12)
) |>
  gt() |>
  fmt_icon(columns = icon) |>
  fmt_integer(columns = count)
category icon count
Documents File Lines 42
Images Image 18
Music Music 95
Videos Video 12

The icon names are replaced with the corresponding Font Awesome icons. This is useful for creating visual category indicators or status displays.

You can customize icon appearance with color options:

dplyr::tibble(
  status = c("Success", "Warning", "Error", "Info"),
  icon = c("check-circle", "exclamation-triangle", "times-circle", "info-circle"),
  color = c("green", "orange", "red", "blue")
) |>
  gt() |>
  fmt_icon(columns = icon, fill_color = from_column("color")) |>
  cols_hide(columns = color)
status icon
Success Circle Check
Warning Triangle Exclamation
Error Circle Xmark
Info Circle Info

Each icon is colored according to its status, creating an immediately recognizable visual language for status indicators.

To explore all available icons, use info_icons():

info_icons()

4.5 Scientific and technical notation

4.5.1 fmt_units()

Scientific and technical writing often requires properly formatted units with superscripts, subscripts, and special symbols. The fmt_units() function interprets a simple text notation and renders it with correct typography.

Note

The units notation described here is the same notation used in column labels (see the section on Incorporating units with gt’s units notation in Chapter 2). That section provides additional details on Greek letters, automatic symbol conversions, chemical formulas, and text formatting options.

Here is the function’s signature:

fmt_units(
  data,
  columns = everything(),
  rows = everything(),
  pattern = "{x}"
)

Let’s format a table of physical quantities with their SI units:

dplyr::tibble(
  quantity = c("Acceleration", "Energy", "Pressure", "Density"),
  unit = c("m/s^2", "kg*m^2/s^2", "N/m^2", "kg/m^3")
) |>
  gt() |>
  fmt_units(columns = unit)
quantity unit
Acceleration m/s2
Energy kg*m2/s
Pressure N/m2
Density kg/m3

The unit strings are rendered with proper formatting: exponents become superscripts, the asterisk becomes a multiplication dot, and the slash indicates division. This notation follows the conventions of scientific typesetting without requiring HTML or LaTeX markup in your data.

The units notation uses a simple but expressive syntax. Here are the key elements:

Notation Meaning Example Input Renders As
^ Superscript (exponent) m^2
_ Subscript x_0 x₀
* Multiplication (·) kg*m kg·m
/ Division (per) m/s m/s
() Grouping J/(kg*K) J/(kg·K)
{{}} Subscript text x_{{avg}} xavg
- Minus in exponent m^-1 m⁻¹

These elements can be combined to express complex units:

dplyr::tibble(
  notation = c("m/s^2", "kg*m^2/s^2", "W/(m^2*K)", "mol^-1", "m^3/(kg*s^2)"),
  description = c(
    "Acceleration", 
    "Energy (Joules)", 
    "Heat transfer coefficient",
    "Per mole",
    "Gravitational constant units"
  )
) |>
  gt() |>
  fmt_units(columns = notation) |>
  cols_move(columns = description, after = notation) |>
  cols_label(notation = "Rendered Unit", description = "Physical Quantity")
Rendered Unit Physical Quantity
m/s2 Acceleration
kg*m2/s Energy (Joules)
W/(m2*K) Heat transfer coefficient
mol−1 Per mole
m3/(kg*s Gravitational constant units

The notation is intuitive for anyone familiar with scientific writing. You write units almost as you would speak them. So “meters per second squared” becomes m/s^2, and “watts per square meter kelvin” becomes W/(m^2*K).

The unit notation supports a rich syntax for complex expressions:

dplyr::tibble(
  quantity = c("Heat capacity", "Thermal conductivity", "Viscosity"),
  unit = c("J/(kg*K)", "W/(m*K)", "kg/(m*s)")
) |>
  gt() |>
  fmt_units(columns = unit)
quantity unit
Heat capacity J/(kg*K)
Thermal conductivity W/(m*K)
Viscosity kg/(m*s)

Parentheses group terms correctly, ensuring the denominator is rendered as a single unit. This creates publication-ready unit formatting directly from simple text notation.

4.5.2 fmt_chem()

Chemical formulas require specific formatting: subscripts for atom counts, superscripts for charges, and proper handling of isotopes and reactions. The fmt_chem() function interprets chemistry notation and renders it correctly.

Here is the function’s signature:

fmt_chem(
  data,
  columns = everything(),
  rows = everything(),
  pattern = "{x}"
)

The chemistry notation that gt uses should be intuitive for anyone familiar with chemical formulas. Here’s a comprehensive reference:

Formatting with fmt_chem()
CHEMICAL FORMULAS
CH3O2 CH3O2
(NH4)2S (NH4)2S
Ca3(PO4)2 Ca3(PO4)2
Sb2O3 Sb2O3
CHARGES
H+ H+
[AgCl2]- [AgCl2]
CrO4^2- CrO42−
CO3^2- CO32−
Y^99+ Y99+
STOICHIOMETRY
0.5 H2O 0.5 H2O
2H2O2 2 H2O2
2 H2O 2 H2O
NUCLIDES/ISOTOPES
^{230}_{90}Th+ 230
90
Th+
^65Cu^{2+} 65
 
Cu2+
^27_14Si13 27
14
Si13
^{0}_{-1}n^{-} 0
-1
n
^0_-1n- 0
-1
n
VARIABLES ITALICIZED
NO_x NOx
Fe^n+ Fen+
x Na(NH4)HPO4 x Na(NH4)HPO4
*n* H2O n H2O
*n*-C5H12 n−C5H12
GREEK CHARACTERS
:delta: ^13C δ13
 
C
:mu:-Cl µ−Cl
PRECIPITATES AND GASES
SO4^2- + Ba^2+ -> BaSO4 v SO42− + Ba2+ BaSO4
A v B (v) -> B ^ B (^) A↓ B↓ B↑ B↑
ADDITION COMPOUNDS
KCr(SO4)2 * 12 H2O KCr(SO4)2⋅12 H2O
KCl . MgCl2 . 6 H2O KCl⋅MgCl2⋅6 H2O
CHEMICAL REACTIONS
2CH3OH -> CH3OCH3 + H2O 2 CH3OH CH3OCH3 + H2O
O3 -> O(^1D) + O2 O3 O(1
 
D) + O2
H2(g) + I2(g) <=> 2HI (g) H2(g) + I2(g) 2 HI (g)
BONDS
C6H5-CHO C6H5−CHO
CH3CH=CH2 CH3CH=CH2

Let’s see basic formula formatting in action:

dplyr::tibble(
  name = c("Water", "Sulfuric acid", "Glucose", "Calcium carbonate"),
  formula = c("H2O", "H2SO4", "C6H12O6", "CaCO3")
) |>
  gt() |>
  fmt_chem(columns = formula)
name formula
Water H2O
Sulfuric acid H2SO4
Glucose C6H12O6
Calcium carbonate CaCO3

The numbers in the formulas become subscripts, transforming "H2O" into the properly typeset "H₂O". This is essential for any table presenting chemical data.

Ionic compounds and charges are handled elegantly:

dplyr::tibble(
  ion = c("Hydroxide", "Sulfate", "Ammonium", "Phosphate", "Iron(III)"),
  formula = c("OH^-", "SO4^2-", "NH4^+", "PO4^3-", "Fe^3+")
) |>
  gt() |>
  fmt_chem(columns = formula)
ion formula
Hydroxide OH
Sulfate SO42−
Ammonium NH4+
Phosphate PO43−
Iron(III) Fe3+

Charges are rendered as superscripts with the appropriate sign, creating properly formatted ionic formulas suitable for scientific publication.

For isotope notation (common in nuclear chemistry and radiochemistry) the function supports full nuclide representation:

dplyr::tibble(
  isotope = c("Carbon-14", "Uranium-235", "Thorium-227", "Deuterium"),
  notation = c("^14_6C", "^235_92U", "^{227}_{90}Th", "^2_1H")
) |>
  gt() |>
  fmt_chem(columns = notation)
isotope notation
Carbon-14 14
6
C
Uranium-235 235
92
U
Thorium-227 227
90
Th
Deuterium 2
1
H

The mass number appears as a superscript and the atomic number as a subscript, positioned before the element symbol: the standard convention for nuclide notation.

Chemical reactions can be expressed with various arrow styles:

dplyr::tibble(
  reaction_type = c(
    "Forward reaction",
    "Reversible reaction", 
    "Equilibrium",
    "Equilibrium (forward favored)"
  ),
  reaction = c(
    "2H2 + O2 -> 2H2O",
    "N2 + 3H2 <--> 2NH3",
    "CH3COOH + H2O <=> CH3COO^- + H3O^+",
    "HCl + H2O <=>> Cl^- + H3O^+"
  )
) |>
  gt() |>
  fmt_chem(columns = reaction)
reaction_type reaction
Forward reaction 2 H2 + O2 2 H2O
Reversible reaction N2 + 3 H2 2 NH3
Equilibrium CH3COOH + H2O CH3COO + H3O+
Equilibrium (forward favored) HCl + H2O Cl + H3O+

Each arrow type has a specific meaning in chemistry: -> for irreversible reactions, <--> for reversible reactions, <=> for equilibrium, and <=>> or <<=> for equilibria that favor one direction.

Hydrates and addition compounds use the centered dot notation:

dplyr::tibble(
  name = c(
    "Copper(II) sulfate pentahydrate",
    "Magnesium sulfate heptahydrate",
    "Chrome alum"
  ),
  formula = c(
    "CuSO4 . 5 H2O",
    "MgSO4 . 7 H2O",
    "KCr(SO4)2 . 12 H2O"
  )
) |>
  gt() |>
  fmt_chem(columns = formula)
name formula
Copper(II) sulfate pentahydrate CuSO4⋅5 H2O
Magnesium sulfate heptahydrate MgSO4⋅7 H2O
Chrome alum KCr(SO4)2⋅12 H2O

The period surrounded by spaces becomes a centered dot (·), the standard notation for waters of hydration and other addition compounds.

4.6 Summary

This chapter has explored the formatting functions that handle non-numeric data: the dates, times, text, URLs, images, and special elements that bring tables to life beyond raw numbers.

The key capabilities we’ve covered:

  • temporal formatting: fmt_date(), fmt_time(), and fmt_datetime() provide 41 date styles and 25 time styles, with full locale support for international audiences. Custom formatting through CLDR patterns or strptime codes gives you complete control when preset styles aren’t enough. The fmt_duration() function transforms raw seconds into human-readable time spans.
  • text and links: fmt_markdown() renders rich text formatting within cells, while fmt_url() and fmt_email() create clickable links that connect your tables to external resources.
  • visual elements: fmt_image() embeds graphics directly in cells, fmt_flag() converts country codes to instantly recognizable flag icons, and fmt_icon() adds Font Awesome icons for status indicators and categorical markers.
  • scientific notation: fmt_units() renders physical units with proper superscripts and subscripts, while fmt_chem() handles chemical formulas, isotopes, and reaction equations with publication-ready typography.
  • country handling: fmt_flag() and fmt_country() work together to present international data with visual clarity, supporting localized country names and tooltips.

Together with the numeric formatters from the previous chapter, you now have a complete toolkit for transforming raw data values into polished, meaningful presentations. But formatting is just the first stage of gt’s rendering pipeline.

The next chapter introduces substitution and text transformation functions. These operate after formatting, allowing you to replace specific values (like missing data or zeros) with alternative text, and to transform the final string representation of cell values. This three-stage pipeline (format, substitute, transform) gives you precise control over exactly how every value appears in your finished table.