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.1gt_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 tablestable_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 groupsales_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 programmaticallyregions<-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 formattingregion_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 groupregional_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.1gt_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.
# Create a table with many rowslarge_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 eachsplit_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 pointscustom_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.1grp_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 groupbase_group<-gt_group(table_north)# Add another tableexpanded_group<-grp_add(base_group, table_south)# Add multiple tables at onceanother_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.2grp_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 groupsecond_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.3grp_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 tableupdated_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 groupupdated_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.4grp_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 tablesthree_table_group<-gt_group(table_north, table_south, another_table)# Remove the middle tabletwo_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.5grp_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 tablecloned_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.1grp_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 groupstyled_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 functionapply_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 groupcorporate_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 grouptables_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 stylingsupercar_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 tablesummary_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 tabledetail_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 reportreport_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:
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 formatsgtsave(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.