11  Working with table groups

Sometimes you need to work with multiple related tables rather than a single table. The gt package provides the gt_group class and associated functions for bundling multiple gt tables together. This enables you to apply common options across tables, manage collections programmatically, and output them together as a cohesive unit.

11.1 Why use table groups?

Table groups are useful when you have:

  • related tables that should be presented together (e.g., results by category)
  • multiple views of the same data (e.g., summary and detail tables)
  • paginated content where a large table needs to be split across pages
  • consistent styling requirements across a set of tables

When you print a gt_group object in HTML, tables are separated by line breaks. In paginated formats (PDF, Word), they’re separated by page breaks. This makes table groups ideal for reports that need multiple tables with consistent formatting.

11.2 Creating table groups

11.2.1 gt_group()

The gt_group() function creates a container that holds multiple gt tables. You can pass tables directly or add them later.

Here is the function’s signature:

gt_group(
  ...,
  .list = list2(...)
)

Let’s create a simple group of two tables:

# Create individual tables
table_north <- 
  dplyr::tibble(
    city = c("New York", "Boston", "Chicago"),
    sales = c(125000, 87000, 95000)
  ) |>
  gt() |>
  tab_header(title = "North Region Sales") |>
  fmt_currency(columns = sales, currency = "USD")

table_south <- 
  dplyr::tibble(
    city = c("Miami", "Atlanta", "Dallas"),
    sales = c(110000, 78000, 92000)
  ) |>
  gt() |>
  tab_header(title = "South Region Sales") |>
  fmt_currency(columns = sales, currency = "USD")

# Combine into a group
sales_group <- gt_group(table_north, table_south)
sales_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
South Region Sales
city sales
Miami $110,000.00
Atlanta $78,000.00
Dallas $92,000.00

The two tables are now bundled together and will display sequentially.

11.2.2 Creating groups from lists

When you have tables stored in a list, use the .list argument:

# Create a list of tables programmatically
regions <- c("East", "West", "Central")
region_data <- list(
  dplyr::tibble(store = c("Store A", "Store B"), revenue = c(50000, 45000)),
  dplyr::tibble(store = c("Store C", "Store D"), revenue = c(62000, 58000)),
  dplyr::tibble(store = c("Store E", "Store F"), revenue = c(41000, 39000))
)

# Create tables with consistent formatting
region_tables <- lapply(seq_along(regions), function(i) {
  region_data[[i]] |>
    gt() |>
    tab_header(title = paste(regions[i], "Region")) |>
    fmt_currency(columns = revenue, currency = "USD")
})

# Combine into a group
regional_group <- gt_group(.list = region_tables)
regional_group
East Region
store revenue
Store A $50,000.00
Store B $45,000.00
West Region
store revenue
Store C $62,000.00
Store D $58,000.00
Central Region
store revenue
Store E $41,000.00
Store F $39,000.00

This pattern is particularly useful when generating tables from grouped data frames or when the number of tables is determined dynamically.

11.3 Splitting tables into groups

11.3.1 gt_split()

The gt_split() function divides a single gt table into multiple tables based on row count. This is useful for pagination or when you need to break up large tables.

Here is the function’s signature:

gt_split(
  data,
  row_every_n = NULL,
  row_slice_i = NULL,
  col_slice_at = NULL
)

Let’s split a large table into smaller chunks:

# Create a table with many rows
large_table <- 
  gtcars |>
  dplyr::select(mfr, model, year, hp, mpg_c) |>
  dplyr::slice(1:12) |>
  gt() |>
  tab_header(title = "Performance Vehicles") |>
  fmt_integer(columns = c(year, hp)) |>
  fmt_number(columns = mpg_c, decimals = 1)

# Split into groups of 4 rows each
split_tables <- gt_split(large_table, row_every_n = 4)
split_tables
Performance Vehicles
mfr model year hp mpg_c
Ford GT 2,017 647 11.0
Ferrari 458 Speciale 2,015 597 13.0
Ferrari 458 Spider 2,015 562 13.0
Ferrari 458 Italia 2,014 562 13.0
Performance Vehicles
mfr model year hp mpg_c
Ferrari 488 GTB 2,016 661 15.0
Ferrari California 2,015 553 16.0
Ferrari GTC4Lusso 2,017 680 12.0
Ferrari FF 2,015 652 11.0
Performance Vehicles
mfr model year hp mpg_c
Ferrari F12Berlinetta 2,015 731 11.0
Ferrari LaFerrari 2,015 949 12.0
Acura NSX 2,017 573 21.0
Nissan GT-R 2,016 545 16.0

The original 12-row table is now split into three tables of 4 rows each. Headers and formatting are preserved in each split table.

11.3.1.1 Custom row slices

For more control over where splits occur, use row_slice_i with a vector of row indices:

# Split at specific points
custom_split <- gt_split(
  large_table,
  row_slice_i = c(5, 9)  
)
custom_split
Performance Vehicles
mfr model year hp mpg_c
Ford GT 2,017 647 11.0
Ferrari 458 Speciale 2,015 597 13.0
Ferrari 458 Spider 2,015 562 13.0
Ferrari 458 Italia 2,014 562 13.0
Ferrari 488 GTB 2,016 661 15.0
Performance Vehicles
mfr model year hp mpg_c
Ferrari California 2,015 553 16.0
Ferrari GTC4Lusso 2,017 680 12.0
Ferrari FF 2,015 652 11.0
Ferrari F12Berlinetta 2,015 731 11.0
Performance Vehicles
mfr model year hp mpg_c
Ferrari LaFerrari 2,015 949 12.0
Acura NSX 2,017 573 21.0
Nissan GT-R 2,016 545 16.0

This creates three tables: rows 1-4, rows 5-8, and rows 9-12. This is useful when you want splits at logical breakpoints rather than fixed intervals.

11.4 Managing tables in a group

11.4.1 grp_add()

Add one or more tables to an existing group with grp_add().

Here is the function’s signature:

grp_add(
  data,
  ...,
  .list = list2(...)
)
# Start with a group
base_group <- gt_group(table_north)

# Add another table
expanded_group <- grp_add(base_group, table_south)

# Add multiple tables at once
another_table <- 
  dplyr::tibble(
    city = c("Seattle", "Portland"),
    sales = c(65000, 48000)
  ) |>
  gt() |>
  tab_header(title = "Pacific Northwest Sales") |>
  fmt_currency(columns = sales, currency = "USD")

final_group <- grp_add(expanded_group, another_table)
final_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
South Region Sales
city sales
Miami $110,000.00
Atlanta $78,000.00
Dallas $92,000.00
Pacific Northwest Sales
city sales
Seattle $65,000.00
Portland $48,000.00

Tables are added at the end of the group by default.

11.4.2 grp_pull()

Extract a specific table from a group with grp_pull(). This returns a standard gt object that you can modify further.

Here is the function’s signature:

grp_pull(
  data,
  which
)
# Extract the second table from the group
second_table <- grp_pull(sales_group, which = 2)
second_table
South Region Sales
city sales
Miami $110,000.00
Atlanta $78,000.00
Dallas $92,000.00

The which argument specifies the position (1-indexed) of the table to extract.

11.4.3 grp_replace()

Replace a table in the group with a new one using grp_replace().

Here is the function’s signature:

grp_replace(
  data,
  ...,
  .list = list2(...)
)
# Create an updated version of the south table
updated_south <- 
  dplyr::tibble(
    city = c("Miami", "Atlanta", "Dallas", "Houston"),
    sales = c(115000, 82000, 98000, 105000)
  ) |>
  gt() |>
  tab_header(title = "South Region Sales (Updated)") |>
  fmt_currency(columns = sales, currency = "USD")

# Replace the second table in the group
updated_group <- grp_replace(sales_group, updated_south, .which = 2)
updated_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
South Region Sales (Updated)
city sales
Miami $115,000.00
Atlanta $82,000.00
Dallas $98,000.00
Houston $105,000.00

The .which argument specifies which table position to replace.

11.4.4 grp_rm()

Remove one or more tables from a group with grp_rm().

Here is the function’s signature:

grp_rm(
  data,
  which
)
# Create a group with three tables
three_table_group <- gt_group(table_north, table_south, another_table)

# Remove the middle table
two_table_group <- grp_rm(three_table_group, which = 2)
two_table_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
Pacific Northwest Sales
city sales
Seattle $65,000.00
Portland $48,000.00

The remaining tables are renumbered automatically.

11.4.5 grp_clone()

Create copies of tables within a group using grp_clone(). This is useful when you want variations of the same base table.

Here is the function’s signature:

grp_clone(
  data,
  which
)
# Clone the first table
cloned_group <- grp_clone(sales_group, which = 1)
cloned_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
South Region Sales
city sales
Miami $110,000.00
Atlanta $78,000.00
Dallas $92,000.00
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00

The cloned table is added at the end of the group. You can then modify it with grp_pull(), make changes, and use grp_replace() to update it.

11.5 Applying options across a group

11.5.1 grp_options()

Apply tab_options() settings to all tables in a group at once with grp_options(). This ensures consistent styling across your table collection.

Here is the function’s signature:

grp_options(
  data,
  ...
)
# Apply consistent styling to all tables in the group
styled_group <- 
  sales_group |>
  grp_options(
    heading.background.color = "steelblue",
    heading.title.font.size = px(16),
    column_labels.font.weight = "bold",
    table.font.size = px(12)
  )
styled_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
South Region Sales
city sales
Miami $110,000.00
Atlanta $78,000.00
Dallas $92,000.00

All tables in the group now share the same styling options. This is more efficient than applying options to each table individually, and it ensures consistency.

11.5.1.1 Common styling patterns

Here’s a pattern for creating a professionally styled group:

# Create a base style function
apply_corporate_style <- function(group) {
  group |>
    grp_options(
      table.border.top.color = "#003366",
      table.border.top.width = px(3),
      heading.background.color = "#003366",
      heading.title.font.size = px(14),
      column_labels.background.color = "#E6E6E6",
      row.striping.include_table_body = TRUE
    )
}

# Apply to any group
corporate_group <- apply_corporate_style(sales_group)
corporate_group
North Region Sales
city sales
New York $125,000.00
Boston $87,000.00
Chicago $95,000.00
South Region Sales
city sales
Miami $110,000.00
Atlanta $78,000.00
Dallas $92,000.00

11.6 Practical workflows

11.6.1 Generating tables from grouped data

A common pattern is creating a table for each group in your data:

# Group data and create a table for each group
tables_by_mfr <- 
  gtcars |>
  dplyr::filter(mfr %in% c("Ferrari", "Lamborghini", "Porsche")) |>
  dplyr::select(mfr, model, year, hp, msrp) |>
  dplyr::group_by(mfr) |>
  dplyr::group_split() |>
  lapply(function(df) {
    manufacturer <- unique(df$mfr)
    df |>
      dplyr::select(-mfr) |>
      gt() |>
      tab_header(title = paste(manufacturer, "Models")) |>
      fmt_integer(columns = c(year, hp)) |>
      fmt_currency(columns = msrp, currency = "USD", decimals = 0)
  })

# Combine into a group with consistent styling
supercar_group <- 
  gt_group(.list = tables_by_mfr) |>
  grp_options(
    heading.background.color = "#1a1a1a",
    column_labels.font.weight = "bold"
  )
supercar_group
Ferrari Models
model year hp msrp
458 Speciale 2,015 597 $291,744
458 Spider 2,015 562 $263,553
458 Italia 2,014 562 $233,509
488 GTB 2,016 661 $245,400
California 2,015 553 $198,973
GTC4Lusso 2,017 680 $298,000
FF 2,015 652 $295,000
F12Berlinetta 2,015 731 $319,995
LaFerrari 2,015 949 $1,416,362
Lamborghini Models
model year hp msrp
Aventador 2,015 700 $397,500
Huracan 2,015 610 $237,250
Gallardo 2,014 550 $191,900
Porsche Models
model year hp msrp
718 Boxster 2,017 300 $56,000
718 Cayman 2,017 300 $53,900
911 2,016 350 $84,300
Panamera 2,016 310 $78,100

11.6.2 Creating summary and detail table pairs

Another common pattern is pairing summary tables with detailed views:

# Summary table
summary_table <- 
  gtcars |>
  dplyr::group_by(mfr) |>
  dplyr::summarize(
    n_models = n(),
    avg_hp = mean(hp),
    avg_price = mean(msrp),
    .groups = "drop"
  ) |>
  dplyr::slice_max(n_models, n = 5) |>
  gt() |>
  tab_header(
    title = "Top Manufacturers by Model Count",
    subtitle = "Summary Statistics"
  ) |>
  fmt_integer(columns = c(n_models, avg_hp)) |>
  fmt_currency(columns = avg_price, currency = "USD", decimals = 0) |>
  cols_label(
    mfr = "Manufacturer",
    n_models = "Models",
    avg_hp = "Avg HP",
    avg_price = "Avg Price"
  )

# Detail table
detail_table <- 
  gtcars |>
  dplyr::filter(mfr == "Porsche") |>
  dplyr::select(model, year, hp, msrp) |>
  gt() |>
  tab_header(
    title = "Porsche Model Details",
    subtitle = "Full specification list"
  ) |>
  fmt_integer(columns = c(year, hp)) |>
  fmt_currency(columns = msrp, currency = "USD", decimals = 0)

# Combine into a report
report_group <- gt_group(summary_table, detail_table)
report_group
Top Manufacturers by Model Count
Summary Statistics
Manufacturer Models Avg HP Avg Price
Ferrari 9 661 $395,837
Audi 5 482 $98,700
BMW 5 443 $98,240
Aston Martin 4 540 $201,761
Porsche 4 315 $68,075
Porsche Model Details
Full specification list
model year hp msrp
718 Boxster 2,017 300 $56,000
718 Cayman 2,017 300 $53,900
911 2,016 350 $84,300
Panamera 2,016 310 $78,100

11.6.3 Building comparative tables

When comparing multiple categories or time periods:

# Create comparison tables for different years
years <- c(2015, 2016, 2017)

year_tables <- lapply(years, function(yr) {
  gtcars |>
    dplyr::filter(year == yr) |>
    dplyr::group_by(mfr) |>
    dplyr::summarize(
      models = n(),
      avg_hp = mean(hp),
      .groups = "drop"
    ) |>
    dplyr::slice_max(models, n = 3) |>
    gt() |>
    tab_header(title = paste(yr, "Top Manufacturers")) |>
    fmt_integer(columns = c(models, avg_hp)) |>
    cols_label(
      mfr = "Manufacturer",
      models = "Models",
      avg_hp = "Avg HP"
    )
})

comparison_group <- 
  gt_group(.list = year_tables) |>
  grp_options(
    column_labels.background.color = "#f0f0f0"
  )
comparison_group
2015 Top Manufacturers
Manufacturer Models Avg HP
Ferrari 6 674
Lamborghini 2 655
Audi 1 430
2016 Top Manufacturers
Manufacturer Models Avg HP
BMW 5 443
Audi 4 495
Aston Martin 3 517
Maserati 3 401
2017 Top Manufacturers
Manufacturer Models Avg HP
Porsche 2 300
Acura 1 573
Aston Martin 1 608
Dodge 1 645
Ferrari 1 680
Ford 1 647
Lotus 1 400
Tesla 1 259

11.7 Output considerations

When outputting table groups:

  • HTML: Tables are separated by <br> tags (line breaks)
  • PDF/LaTeX: Tables are separated by page breaks
  • Word: Tables are separated by page breaks
  • RTF: Tables are separated by page breaks

This behavior makes table groups ideal for generating reports where each table should appear on its own page in printed output.

# Save a table group to different formats
gtsave(sales_group, "sales_report.html")
gtsave(sales_group, "sales_report.pdf")
gtsave(sales_group, "sales_report.docx")

Each output format handles the group appropriately for its medium.

11.8 Summary

This chapter has introduced table groups: containers that hold multiple gt tables and treat them as a coordinated unit.

The key concepts we’ve covered:

  • creating groups: gt_group() bundles multiple gt tables together, either by passing them directly or building the collection incrementally.
  • managing tables: grp_add() adds tables to existing groups, grp_pull() extracts individual tables, grp_replace() swaps tables, grp_rm() removes tables, and grp_clone() duplicates tables within a group.
  • splitting tables: gt_split() divides a single large table into multiple smaller tables based on row count, column count, or row groups (useful for pagination or breaking up dense displays).
  • common options: grp_options() applies tab_options() settings across all tables in a group, ensuring visual consistency without repetitive code.
  • output behavior: in HTML, grouped tables are separated by line breaks. In paginated formats (PDF, Word, RTF), they’re separated by page breaks, making groups ideal for multi-page reports.

Table groups solve practical problems: presenting related analyses together, maintaining consistent styling across report sections, and handling pagination for large datasets. They complement rather than replace individual table construction. You build each table with the full power of gt, then combine them into groups for coordinated output.

The next chapter explores output formats, covering how to render gt tables for different destinations: HTML for web, PDF for print, Word for documents, and more. You’ll learn to optimize tables for each format’s unique characteristics and constraints.