Skip to contents

Build the column-header band(s) above the rendered table. Each named argument is one band; the value is either a character vector of column names (leaf band) or a named list of further bands (inner band). Nesting depth is arbitrary — the engine renders one band row per depth level, with each cell spanning the columns of its leaves.

Usage

headers(.spec, ...)

Arguments

.spec

The tabular_spec to attach the header tree to. <tabular_spec>: required. Dot-prefixed so R's partial argument matching cannot accidentally bind a short user-supplied band label in ... to the spec slot.

...

Named header bands. Each name is the band label (must be non-blank); each value is either:

  • a character vector of data-column names — leaf band, or

  • a named list whose entries follow the same recursive pattern — inner band.

Inside a nested-list value, an unnamed character-vector entry declares a passthrough leaf (see the Passthrough section below).

Restriction: Every column referenced must exist in .spec@data. A column may appear under at most one leaf. Names must be unique within one headers() call. Tip: Pass headers() with no arguments to clear the tree. Interaction: Band labels support glue-style {expr} interpolation, evaluated as R code in the calling environment at build time (double a brace for a literal one). The non-blank and uniqueness checks apply to the raw author-typed name, before interpolation.

Value

The updated tabular_spec. Continue chaining with sort_rows(), style().

Details

Replace, not stack. A second headers() call REPLACES the prior tree — header structure is a single spec, not a stackable list. Call with no arguments to clear the tree.

Strict label rule. Every declared band label must carry visible text — empty strings, NA, and whitespace-only labels are rejected at every nesting level. This is stricter than col_spec(), which DOES accept empty labels (a row-label column with no header text is a legitimate clinical case). A silently-blank band would be a layout artefact.

Uncovered columns render naked. Columns not referenced under any band render with their col_spec.label only — no extra band row above them. This is the canonical pattern for row-label columns (variable, soc, stat_label).

Multi-line band labels. Embed \n in a band label for a two-line band cell (arm name on row 1, BigN on row 2).

Spanner underline trim (backend limitation). Each spanner's underline is trimmed at both ends, booktabs \cmidrule(lr) style, so adjacent spanners are separated by a visible gap rather than merging into one continuous line. PDF / LaTeX (tabularray leftpos/rightpos) and HTML (an inset rule) render the trim natively. RTF and DOCX cannot inset a cell border horizontally, so there the spanner underline spans the full band width (adjacent spanner rules abut). This is a known, documented limitation of the OOXML / RTF cell-border model, not a bug.

Passthrough leaves inside a nested band

Inside a nested-list value, a child entry may be unnamed — the entry is then a character vector of column names that sit directly under the parent with no intermediate band at this depth. Use this when one column under a band has no sub-grouping while its siblings do. The strict-label rule still applies to every declared band; an unnamed passthrough is NOT a band with a missing label — it is "no band declared at this depth for this column."

See also

Companion verb: cols() / col_spec() sets per-column labels — the leaf-row header text that sits below the band rows this verb builds.

Sibling build verbs: sort_rows(), style(), paginate(), preset().

Entry / terminal verbs: tabular(), emit(), as_grid().

Inline label formatting: md(), html().

Examples

# ---- Example 1: Single "Treatment Group" band over four arms ----
#
# AE-by-SOC/PT table with one flat band labelled "Treatment Group"
# spanning the four arm columns and the Total column. The
# row-label column (`soc`) sits to the left of the band with no
# header covering it — the canonical clinical layout.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)

tabular(
  cdisc_saf_aesocpt,
  titles = c(
    "Table 14.3.1",
    "Adverse Events by System Organ Class and Preferred Term",
    "Safety Population"
  ),
  footnotes = "Subjects are counted once per SOC and once per PT."
) |>
  cols(
    label    = col_spec(label = "SOC / PT", indent = "indent_level"),
    soc      = col_spec(visible = FALSE),
    row_type = col_spec(visible = FALSE),
    soc_n    = col_spec(visible = FALSE),
    n_total  = col_spec(visible = FALSE),
    placebo  = col_spec(label = "Placebo\nN={n['placebo']}"),
    drug_50  = col_spec(label = "Drug 50\nN={n['drug_50']}"),
    drug_100 = col_spec(label = "Drug 100\nN={n['drug_100']}"),
    Total    = col_spec(label = "Total\nN={n['Total']}")
  ) |>
  headers(
    "Treatment Group" = c("placebo", "drug_50", "drug_100", "Total")
  ) |>
  sort_rows(by = c("soc_n", "n_total"), descending = c(TRUE, TRUE))

 

Table 14.3.1

Adverse Events by System Organ Class and Preferred Term

Safety Population

 

Treatment Group
SOC / PTPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Total
N=254
TOTAL SUBJECTS WITH AN EVENT52 (60.5)81 (84.4)66 (91.7)199 (78.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS19 (22.1)36 (37.5)35 (48.6)90 (35.4)
PRURITUS8 (9.3)21 (21.9)25 (34.7)54 (21.3)
ERYTHEMA8 (9.3)14 (14.6)14 (19.4)36 (14.2)
RASH5 (5.8)13 (13.5)8 (11.1)26 (10.2)
HYPERHIDROSIS2 (2.3)4 (4.2)8 (11.1)14 (5.5)
SKIN IRRITATION3 (3.5)6 (6.2)5 (6.9)14 (5.5)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS15 (17.4)36 (37.5)30 (41.7)81 (31.9)
APPLICATION SITE PRURITUS6 (7.0)23 (24.0)21 (29.2)50 (19.7)
APPLICATION SITE ERYTHEMA3 (3.5)13 (13.5)14 (19.4)30 (11.8)
APPLICATION SITE DERMATITIS5 (5.8)9 (9.4)7 (9.7)21 (8.3)
APPLICATION SITE IRRITATION3 (3.5)9 (9.4)9 (12.5)21 (8.3)
APPLICATION SITE VESICLES1 (1.2)5 (5.2)5 (6.9)11 (4.3)
GASTROINTESTINAL DISORDERS13 (15.1)12 (12.5)17 (23.6)42 (16.5)
DIARRHOEA9 (10.5)5 (5.2)3 (4.2)17 (6.7)
VOMITING3 (3.5)4 (4.2)6 (8.3)13 (5.1)
NAUSEA3 (3.5)3 (3.1)6 (8.3)12 (4.7)
ABDOMINAL PAIN1 (1.2)3 (3.1)1 (1.4)5 (2.0)
SALIVARY HYPERSECRETION0 (0.0)0 (0.0)4 (5.6)4 (1.6)
NERVOUS SYSTEM DISORDERS6 (7.0)18 (18.8)17 (23.6)41 (16.1)
DIZZINESS2 (2.3)9 (9.4)10 (13.9)21 (8.3)
HEADACHE3 (3.5)3 (3.1)5 (6.9)11 (4.3)
SYNCOPE0 (0.0)5 (5.2)2 (2.8)7 (2.8)
SOMNOLENCE2 (2.3)3 (3.1)1 (1.4)6 (2.4)
TRANSIENT ISCHAEMIC ATTACK0 (0.0)2 (2.1)1 (1.4)3 (1.2)
CARDIAC DISORDERS7 (8.1)12 (12.5)14 (19.4)33 (13.0)
SINUS BRADYCARDIA2 (2.3)7 (7.3)8 (11.1)17 (6.7)
MYOCARDIAL INFARCTION4 (4.7)2 (2.1)4 (5.6)10 (3.9)
ATRIAL FIBRILLATION1 (1.2)2 (2.1)2 (2.8)5 (2.0)
SUPRAVENTRICULAR EXTRASYSTOLES1 (1.2)1 (1.0)1 (1.4)3 (1.2)
VENTRICULAR EXTRASYSTOLES0 (0.0)2 (2.1)1 (1.4)3 (1.2)
INFECTIONS AND INFESTATIONS12 (14.0)6 (6.2)11 (15.3)29 (11.4)
NASOPHARYNGITIS2 (2.3)4 (4.2)6 (8.3)12 (4.7)
UPPER RESPIRATORY TRACT INFECTION6 (7.0)1 (1.0)3 (4.2)10 (3.9)
INFLUENZA1 (1.2)1 (1.0)1 (1.4)3 (1.2)
URINARY TRACT INFECTION2 (2.3)0 (0.0)1 (1.4)3 (1.2)
CYSTITIS1 (1.2)0 (0.0)1 (1.4)2 (0.8)
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS5 (5.8)8 (8.3)9 (12.5)22 (8.7)
COUGH1 (1.2)5 (5.2)5 (6.9)11 (4.3)
NASAL CONGESTION3 (3.5)1 (1.0)3 (4.2)7 (2.8)
DYSPNOEA1 (1.2)1 (1.0)1 (1.4)3 (1.2)
EPISTAXIS0 (0.0)1 (1.0)2 (2.8)3 (1.2)
PHARYNGOLARYNGEAL PAIN0 (0.0)1 (1.0)1 (1.4)2 (0.8)
PSYCHIATRIC DISORDERS7 (8.1)9 (9.4)3 (4.2)19 (7.5)
CONFUSIONAL STATE2 (2.3)3 (3.1)1 (1.4)6 (2.4)
AGITATION2 (2.3)3 (3.1)0 (0.0)5 (2.0)
INSOMNIA2 (2.3)0 (0.0)2 (2.8)4 (1.6)
ANXIETY0 (0.0)3 (3.1)0 (0.0)3 (1.2)
DELUSION1 (1.2)0 (0.0)1 (1.4)2 (0.8)
MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS3 (3.5)6 (6.2)5 (6.9)14 (5.5)
BACK PAIN1 (1.2)1 (1.0)3 (4.2)5 (2.0)
ARTHRALGIA1 (1.2)2 (2.1)1 (1.4)4 (1.6)
SHOULDER PAIN1 (1.2)2 (2.1)0 (0.0)3 (1.2)
MUSCLE SPASMS0 (0.0)1 (1.0)1 (1.4)2 (0.8)
ARTHRITIS0 (0.0)0 (0.0)1 (1.4)1 (0.4)
INVESTIGATIONS5 (5.8)4 (4.2)3 (4.2)12 (4.7)
ELECTROCARDIOGRAM ST SEGMENT DEPRESSION4 (4.7)1 (1.0)0 (0.0)5 (2.0)
ELECTROCARDIOGRAM T WAVE INVERSION2 (2.3)1 (1.0)1 (1.4)4 (1.6)
BLOOD GLUCOSE INCREASED0 (0.0)1 (1.0)1 (1.4)2 (0.8)
ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED1 (1.2)1 (1.0)0 (0.0)2 (0.8)
BIOPSY0 (0.0)0 (0.0)1 (1.4)1 (0.4)

Subjects are counted once per SOC and once per PT.

# ---- Example 2: Two-level nested band — Control vs Active arms ---- # # Efficacy BOR table where the active arms are grouped under an # "Active" sub-band and the placebo arm under a "Control" # sub-band, both under a single "Treatment Group" parent. # Demonstrates the named-list value form for arbitrary-depth # nesting. bor_levels <- c( "CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING", "ORR (CR + PR)", "CBR (CR + PR + SD)", "DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)" ) eff <- cdisc_eff_resp eff$stat_label <- factor(eff$stat_label, levels = bor_levels) ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short) tabular( eff, titles = c( "Table 14.2.1", "Best Overall Response and Response Rates", "Efficacy Evaluable Population" ), footnotes = "Response per RECIST 1.1, investigator assessment." ) |> cols( stat_label = col_spec(label = "Response"), row_type = col_spec(visible = FALSE), groupid = col_spec(visible = FALSE), group_label = col_spec(visible = FALSE), placebo = col_spec(label = "Placebo\nN={ne['placebo']}"), drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}"), drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}") ) |> headers( "Treatment Group" = list( "Control" = "placebo", "Active" = c("drug_50", "drug_100") ) ) |> sort_rows(by = c("groupid", "stat_label"))

 

Table 14.2.1

Best Overall Response and Response Rates

Efficacy Evaluable Population

 

Treatment Group
ControlActive
ResponsePlacebo
N=86
Drug 50
N=84
Drug 100
N=84
CR1 (1.2)1 (1.2)1 (1.2)
PR1 (1.2)00
SD1 (1.2)00
NON-CR/NON-PD001 (1.2)
PD001 (1.2)
NE01 (1.2)0
MISSING83 (96.5)82 (97.6)81 (96.4)
ORR (CR + PR)2 (2.3)1 (1.2)1 (1.2)
95% CI (Clopper-Pearson)(0.3, 8.1)(0.0, 6.5)(0.0, 6.5)
CBR (CR + PR + SD)3 (3.5)1 (1.2)1 (1.2)
95% CI (Clopper-Pearson)(0.7, 9.9)(0.0, 6.5)(0.0, 6.5)
DCR (CR + PR + SD + NON-CR/NON-PD)3 (3.5)1 (1.2)2 (2.4)
95% CI (Clopper-Pearson)(0.7, 9.9)(0.0, 6.5)(0.3, 8.3)

Response per RECIST 1.1, investigator assessment.

# ---- Example 3: Multiple peer bands side by side ---- # # Vital-signs summary where the parameter columns (param, # paramcd, visit, stat_label) sit on the left under a "Variable" # band, and the arm columns sit on the right under "Treatment # Group". Demonstrates multiple top-level bands in one call -- # bands render side by side in the order declared. vit <- cdisc_saf_vital tabular(vit, titles = c("Table 14.4.1", "Vital Signs Summary")) |> cols( param = col_spec(usage = "group", label = "Parameter"), paramcd = col_spec(visible = FALSE), visit = col_spec(usage = "group", label = "Visit"), stat_label = col_spec(label = "Statistic"), placebo = col_spec(label = "Placebo", align = "decimal"), drug_50 = col_spec(label = "Drug 50", align = "decimal"), drug_100 = col_spec(label = "Drug 100", align = "decimal") ) |> headers( "Variable" = c("param", "paramcd", "visit", "stat_label"), "Treatment Group" = c("placebo", "drug_50", "drug_100") )

 

Table 14.4.1

Vital Signs Summary

 

VariableTreatment Group
StatisticPlaceboDrug 50Drug 100
Diastolic Blood Pressure (mmHg)
Baseline
n340         384         288         
Mean (SD) 77.1 (10.7) 76.6 (9.8)  78.2 (10.3)
Median 77.7        76.7        78.8       
Min, Max 40  , 110   48  , 108   51  , 108  
 
Week 8
n292         240         224         
Mean (SD) 75.2 (9.1)  75.4 (10.6) 77.4 (9.1) 
Median 76.0        74.0        78.3       
Min, Max 49  , 101   52  , 100   54  , 98   
 
Week 16
n272         168         148         
Mean (SD) 75.1 (10.9) 75.2 (10.0) 76.0 (9.0) 
Median 76.0        75.7        77.3       
Min, Max 49  , 98    55  , 98    50  , 92   
 
End of Treatment
n222         177         168         
Mean (SD) 74.4 (10.7) 76.0 (11.2) 76.0 (9.9) 
Median 73.5        76.0        78.0       
Min, Max 49  , 104   50  , 100   56  , 98   
 
Pulse Rate (beats/min)
Baseline
n340         384         288         
Mean (SD) 73.5 (11.6) 72.1 (10.8) 72.4 (9.7) 
Median 72.3        70.0        71.7       
Min, Max 51  , 134   50  , 104   52  , 100  
 
Week 8
n292         240         224         
Mean (SD) 71.8 (9.0)  72.6 (11.1) 74.0 (8.9) 
Median 72.0        72.0        73.2       
Min, Max 52  , 102   49  , 104   50  , 104  
 
Week 16
n272         168         148         
Mean (SD) 70.6 (8.8)  68.8 (9.4)  73.2 (9.5) 
Median 70.2        68.0        72.0       
Min, Max 50  , 90    48  , 104   51  , 96   
 
End of Treatment
n222         177         168         
Mean (SD) 75.2 (11.5) 74.1 (9.4)  73.6 (9.6) 
Median 74.0        75.0        73.0       
Min, Max 51  , 106   50  , 94    50  , 98   
 
Systolic Blood Pressure (mmHg)
Baseline
n340         384         288         
Mean (SD)136.8 (17.6)137.9 (18.5)137.8 (17.2)
Median136.3       138.0       138.0       
Min, Max 80  , 184  100  , 194  100  , 192  
 
Week 8
n292         240         224         
Mean (SD)136.3 (17.0)134.9 (17.8)135.1 (15.5)
Median136.5       132.3       134.0       
Min, Max 90  , 189   92  , 200   91  , 198  
 
Week 16
n272         168         148         
Mean (SD)134.6 (18.3)132.5 (14.3)133.7 (16.0)
Median134.0       130.0       132.0       
Min, Max 76  , 190  100  , 168   99  , 186  
 
End of Treatment
n222         177         168         
Mean (SD)132.7 (15.4)133.0 (17.1)132.3 (15.6)
Median131.0       130.0       131.0       
Min, Max 78  , 172   92  , 178  100  , 177  
 
Temperature (C)
Baseline
n172         190         144         
Mean (SD) 36.6 (0.4)  36.5 (0.4)  36.6 (0.4) 
Median 36.7        36.6        36.6       
Min, Max 35  , 37    35  , 37    36  , 37   
 
Week 8
n146         118         112         
Mean (SD) 36.6 (0.4)  36.6 (0.4)  36.6 (0.4) 
Median 36.6        36.7        36.7       
Min, Max 36  , 37    36  , 37    36  , 37   
 
Week 16
n136          82          74         
Mean (SD) 36.7 (0.3)  36.6 (0.4)  36.6 (0.4) 
Median 36.7        36.6        36.7       
Min, Max 36  , 37    36  , 37    36  , 37   
 
End of Treatment
n 74          59          56         
Mean (SD) 36.7 (0.4)  36.6 (0.4)  36.6 (0.4) 
Median 36.8        36.7        36.7       
Min, Max 35  , 37    35  , 38    36  , 37   
# ---- Example 4: Three-tier band over efficacy arms + Total ---- # # Demographics-style three-tier nesting: top band labels the # whole arm strip, middle band splits Active vs Placebo, leaf # bands carry the per-arm column labels. Each child within a # `list(...)` may itself be a `list(...)` — bands nest to # arbitrary depth using nested list literals. n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short) tabular(cdisc_saf_demo, titles = "Demographics, hierarchical headers") |> cols( variable = col_spec(usage = "group", label = "Characteristic"), stat_label = col_spec(label = "Statistic"), placebo = col_spec(label = "N={n['placebo']}"), drug_50 = col_spec(label = "N={n['drug_50']}"), drug_100 = col_spec(label = "N={n['drug_100']}"), Total = col_spec(label = "N={n['Total']}") ) |> headers( "Treatment Group" = list( "Control" = "placebo", "Active" = list( "Drug 50" = "drug_50", "Drug 100" = "drug_100" ), "Pooled" = "Total" ) )

 

Demographics, hierarchical headers

 

Treatment Group
ControlActivePooled
Drug 50Drug 100
StatisticN=86N=96N=72N=254
Age (years)
n869672254
Mean (SD)75.2 (8.59)76.0 (8.11)73.8 (7.94)75.1 (8.25)
Median76.078.075.577.0
Q1, Q369.2, 81.871.0, 82.070.5, 79.070.0, 81.0
Min, Max52, 8951, 8856, 8851, 89
 
Sex, n (%)
F53 (61.6)55 (57.3)35 (48.6)143 (56.3)
M33 (38.4)41 (42.7)37 (51.4)111 (43.7)
 
Race, n (%)
WHITE78 (90.7)90 (93.8)62 (86.1)230 (90.6)
BLACK OR AFRICAN AMERICAN8 (9.3)6 (6.2)9 (12.5)23 (9.1)
ASIAN0 (0.0)0 (0.0)0 (0.0)0 (0.0)
AMERICAN INDIAN OR ALASKA NATIVE0 (0.0)0 (0.0)1 (1.4)1 (0.4)