Header, Footer, and Stub

Three structural components frame the data in your table: the Table Header introduces it with a title and optional subtitle, the Table Footer anchors it with source notes or other supplementary information, and the Stub provides a left-hand column of row identifiers. All three are added using the tab_*() family of methods.

A well-structured frame transforms a bare data grid into something self-explanatory. Without these components, your reader has to look outside the table for context: what is this data? Where did it come from? What do the rows represent? Adding a header, footer, and stub answers those questions inside the table itself, so it can stand on its own in a report, slide deck, or web page.

Adding a Table Header

A Table Header is easy to add with the tab_header() method. Let’s see how a basic table looks with a title and a subtitle:

from great_tables import GT, md, html
from great_tables.data import islands

islands_mini = islands.head(10)

# Make a display table with the `islands_tbl` table and
# put a heading just above the column labels
(
    GT(islands_mini)
    .tab_header(
        title = "Large Landmasses of the World",
        subtitle = "The top ten largest are presented"
    )
)
Large Landmasses of the World
The top ten largest are presented
name size
Africa 11506
Antarctica 5500
Asia 16988
Australia 2968
Axel Heiberg 16
Baffin 184
Banks 23
Borneo 280
Britain 84
Celebes 73

As a rule of thumb, any table that will be seen outside your own notebook benefits from a title. It also immediately orients the reader as to what the table’s all about.

The Header provides an opportunity to describe the data that’s presented. Using subtitle= allows us to insert a subtitle, which is an optional part of the Header. We may also style the title= and subtitle= using Markdown! We do this by wrapping the values passed to title= or subtitle= with the md() helper function (we may also use html() in a similar fashion). Here is an example with the table data truncated for brevity:

# Make a display table with the `islands_tbl` table and
# put a heading just above the column labels
gt_tbl = (
    GT(islands.head(2))
    .tab_header(
        title = md("Large Landmasses of the *World* 🌐"),
        subtitle = md("The top **ten** largest are presented")
    )
)

gt_tbl
Large Landmasses of the World 🌐
The top ten largest are presented
name size
Africa 11506
Antarctica 5500

With a title and subtitle in place, the reader immediately knows what the table is about before looking at any data.

Adding Source Notes

A source note can be added to the table’s Footer through use of the tab_source_note() method. It works in the same way as tab_header() (it also allows for Markdown inputs) except it can be called multiple times. Each invocation results in the addition of a source note.

# Display the `islands_tbl` data with a heading and two source notes
(
    gt_tbl
    .tab_source_note(
        source_note = "Source: The World Almanac and Book of Facts, 1975, page 406."
    )
    .tab_source_note(
        source_note = md("Reference: McNeil, D. R. (1977) *Interactive Data Analysis*. Wiley.")
    )
)
Large Landmasses of the World 🌐
The top ten largest are presented
name size
Africa 11506
Antarctica 5500
Source: The World Almanac and Book of Facts, 1975, page 406.
Reference: McNeil, D. R. (1977) Interactive Data Analysis. Wiley.

Source notes are pretty valuable because they provide provenance and credibility. Academic and business audiences might expect to know where data comes from. Including that information directly in the table saves readers from hunting through surrounding text. Source notes are also a good place for methodological caveats or disclaimers that apply to the entire table.

With just a few method calls, we have added essential context to the table. The title and subtitle tell the reader what data is being presented, and the source notes provide attribution. Together, these components frame the table body and help your audience understand the data at a glance.

The Stub: Row Labels

The Stub is good to have whenever the first column of your data serves as an identifier rather than a measured value. Moving identifiers into the stub visually separates “what this row is about” from “what was measured”, making the table easier to scan. If your rows represent people, places, time periods, or categories, those labels would go nicely in the stub.

The Stub is the area to the left of the table body that typically contains row labels and may also contain row group labels. Those subparts can be grouped in a sequence of row groups. The Stub Head provides a location for a label that describes the Stub (and could also be used to describe the column labels). The Stub is optional since there are cases where it wouldn’t be useful (the display tables presented earlier looked just fine without one).

An easy way to generate a Stub part is by specifying a stub column in the GT() class with the rowname_col= argument. This will signal to Great Tables that the named column should be used as the stub, using the contents of that column to make row labels. Let’s add a stub with our islands dataset by using rowname_col= in the call to GT:

GT(islands_mini).tab_stub(rowname_col="name")
size
Africa 11506
Antarctica 5500
Asia 16988
Australia 2968
Axel Heiberg 16
Baffin 184
Banks 23
Borneo 280
Britain 84
Celebes 73

Notice that the landmass names are now placed to the left? That’s the Stub. Notably, there is a prominent border to the right of it but there’s no label above the Stub. We can change this and apply what’s known as a stubhead label through use of the tab_stubhead() method:

(
    GT(islands_mini)
    .tab_stub(rowname_col="name")
    .tab_stubhead(label="landmass")
)
landmass size
Africa 11506
Antarctica 5500
Asia 16988
Australia 2968
Axel Heiberg 16
Baffin 184
Banks 23
Borneo 280
Britain 84
Celebes 73

A very important thing to note here is that the table now has one column. Before, when there was no Stub, two columns were present (with the Column Labels of "name" and "size") but now column number 1 (the only column remaining) is size.

Row Groups

Grouping is most useful when your data has a natural categorical structure and you want readers to compare within and across categories. Rather than forcing the reader to mentally sort the rows, row groups do that work up front.

Let’s incorporate row groups into the display table. This divides rows into groups, creating row groups, and results in a display of a row group labels right above each group. This can be easily done with a table containing row labels and the key is to use the groupname_col= argument of the GT class. Here we will create three row groups (with row group labels "continent", "country", and "subregion") to have a grouping of rows.

island_groups = islands.head(10).assign(group = ["subregion"] * 2 + ["country"] * 2 + ["continent"] * 6)

(
    GT(island_groups)
    .tab_stub(rowname_col="name", groupname_col="group")
    .tab_stubhead(label="landmass")
)
landmass size
subregion
Africa 11506
Antarctica 5500
country
Asia 16988
Australia 2968
continent
Axel Heiberg 16
Baffin 184
Banks 23
Borneo 280
Britain 84
Celebes 73

The table now groups its rows by continent, country, and subregion, with labels appearing above each group. Row groups make it much easier for readers to scan and compare related entries.

GT Convenience Arguments

Rather than using the tab_stub() method, the GT(rowname_col=..., groupname_col=...) arguments provide a quick way to specify row names and groups.

GT(island_groups, rowname_col="name", groupname_col="group")
size
subregion
Africa 11506
Antarctica 5500
country
Asia 16988
Australia 2968
continent
Axel Heiberg 16
Baffin 184
Banks 23
Borneo 280
Britain 84
Celebes 73

The stub provides a clear organizational framework for your data by separating identifiers from values. Whether you simply need named rows or a fully grouped hierarchy, the combination of rowname_col=, groupname_col=, and tab_stubhead() gives you precise control over how readers navigate your table.