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(label = "Parameter"), paramcd = col_spec(visible = FALSE), visit = col_spec(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
ParameterVisitStatisticPlaceboDrug 50Drug 100
Diastolic Blood Pressure (mmHg)Baselinen340         384         288         
Diastolic Blood Pressure (mmHg)BaselineMean (SD) 77.1 (10.7) 76.6 ( 9.8) 78.2 (10.3)
Diastolic Blood Pressure (mmHg)BaselineMedian 77.7        76.7        78.8       
Diastolic Blood Pressure (mmHg)BaselineMin, Max 40  , 110   48  , 108   51  , 108  
Diastolic Blood Pressure (mmHg)Week 8n292         240         224         
Diastolic Blood Pressure (mmHg)Week 8Mean (SD) 75.2 ( 9.1) 75.4 (10.6) 77.4 ( 9.1)
Diastolic Blood Pressure (mmHg)Week 8Median 76.0        74.0        78.3       
Diastolic Blood Pressure (mmHg)Week 8Min, Max 49  , 101   52  , 100   54  , 98   
Diastolic Blood Pressure (mmHg)Week 16n272         168         148         
Diastolic Blood Pressure (mmHg)Week 16Mean (SD) 75.1 (10.9) 75.2 (10.0) 76.0 ( 9.0)
Diastolic Blood Pressure (mmHg)Week 16Median 76.0        75.7        77.3       
Diastolic Blood Pressure (mmHg)Week 16Min, Max 49  , 98    55  , 98    50  , 92   
Diastolic Blood Pressure (mmHg)End of Treatmentn222         177         168         
Diastolic Blood Pressure (mmHg)End of TreatmentMean (SD) 74.4 (10.7) 76.0 (11.2) 76.0 ( 9.9)
Diastolic Blood Pressure (mmHg)End of TreatmentMedian 73.5        76.0        78.0       
Diastolic Blood Pressure (mmHg)End of TreatmentMin, Max 49  , 104   50  , 100   56  , 98   
Pulse Rate (beats/min)Baselinen340         384         288         
Pulse Rate (beats/min)BaselineMean (SD) 73.5 (11.6) 72.1 (10.8) 72.4 ( 9.7)
Pulse Rate (beats/min)BaselineMedian 72.3        70.0        71.7       
Pulse Rate (beats/min)BaselineMin, Max 51  , 134   50  , 104   52  , 100  
Pulse Rate (beats/min)Week 8n292         240         224         
Pulse Rate (beats/min)Week 8Mean (SD) 71.8 ( 9.0) 72.6 (11.1) 74.0 ( 8.9)
Pulse Rate (beats/min)Week 8Median 72.0        72.0        73.2       
Pulse Rate (beats/min)Week 8Min, Max 52  , 102   49  , 104   50  , 104  
Pulse Rate (beats/min)Week 16n272         168         148         
Pulse Rate (beats/min)Week 16Mean (SD) 70.6 ( 8.8) 68.8 ( 9.4) 73.2 ( 9.5)
Pulse Rate (beats/min)Week 16Median 70.2        68.0        72.0       
Pulse Rate (beats/min)Week 16Min, Max 50  , 90    48  , 104   51  , 96   
Pulse Rate (beats/min)End of Treatmentn222         177         168         
Pulse Rate (beats/min)End of TreatmentMean (SD) 75.2 (11.5) 74.1 ( 9.4) 73.6 ( 9.6)
Pulse Rate (beats/min)End of TreatmentMedian 74.0        75.0        73.0       
Pulse Rate (beats/min)End of TreatmentMin, Max 51  , 106   50  , 94    50  , 98   
Systolic Blood Pressure (mmHg)Baselinen340         384         288         
Systolic Blood Pressure (mmHg)BaselineMean (SD)136.8 (17.6)137.9 (18.5)137.8 (17.2)
Systolic Blood Pressure (mmHg)BaselineMedian136.3       138.0       138.0       
Systolic Blood Pressure (mmHg)BaselineMin, Max 80  , 184  100  , 194  100  , 192  
Systolic Blood Pressure (mmHg)Week 8n292         240         224         
Systolic Blood Pressure (mmHg)Week 8Mean (SD)136.3 (17.0)134.9 (17.8)135.1 (15.5)
Systolic Blood Pressure (mmHg)Week 8Median136.5       132.3       134.0       
Systolic Blood Pressure (mmHg)Week 8Min, Max 90  , 189   92  , 200   91  , 198  
Systolic Blood Pressure (mmHg)Week 16n272         168         148         
Systolic Blood Pressure (mmHg)Week 16Mean (SD)134.6 (18.3)132.5 (14.3)133.7 (16.0)
Systolic Blood Pressure (mmHg)Week 16Median134.0       130.0       132.0       
Systolic Blood Pressure (mmHg)Week 16Min, Max 76  , 190  100  , 168   99  , 186  
Systolic Blood Pressure (mmHg)End of Treatmentn222         177         168         
Systolic Blood Pressure (mmHg)End of TreatmentMean (SD)132.7 (15.4)133.0 (17.1)132.3 (15.6)
Systolic Blood Pressure (mmHg)End of TreatmentMedian131.0       130.0       131.0       
Systolic Blood Pressure (mmHg)End of TreatmentMin, Max 78  , 172   92  , 178  100  , 177  
Temperature (C)Baselinen172         190         144         
Temperature (C)BaselineMean (SD) 36.6 ( 0.4) 36.5 ( 0.4) 36.6 ( 0.4)
Temperature (C)BaselineMedian 36.7        36.6        36.6       
Temperature (C)BaselineMin, Max 35  , 37    35  , 37    36  , 37   
Temperature (C)Week 8n146         118         112         
Temperature (C)Week 8Mean (SD) 36.6 ( 0.4) 36.6 ( 0.4) 36.6 ( 0.4)
Temperature (C)Week 8Median 36.6        36.7        36.7       
Temperature (C)Week 8Min, Max 36  , 37    36  , 37    36  , 37   
Temperature (C)Week 16n136          82          74         
Temperature (C)Week 16Mean (SD) 36.7 ( 0.3) 36.6 ( 0.4) 36.6 ( 0.4)
Temperature (C)Week 16Median 36.7        36.6        36.7       
Temperature (C)Week 16Min, Max 36  , 37    36  , 37    36  , 37   
Temperature (C)End of Treatmentn 74          59          56         
Temperature (C)End of TreatmentMean (SD) 36.7 ( 0.4) 36.6 ( 0.4) 36.6 ( 0.4)
Temperature (C)End of TreatmentMedian 36.8        36.7        36.7       
Temperature (C)End of TreatmentMin, 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(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
CharacteristicStatisticN=86N=96N=72N=254
Age (years)n869672254
Age (years)Mean (SD)75.2 (8.59)76.0 (8.11)73.8 (7.94)75.1 (8.25)
Age (years)Median76.078.075.577.0
Age (years)Q1, Q369.2, 81.871.0, 82.070.5, 79.070.0, 81.0
Age (years)Min, Max52, 8951, 8856, 8851, 89
Sex, n (%)F53 (61.6)55 (57.3)35 (48.6)143 (56.3)
Sex, n (%)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)
Race, n (%)BLACK OR AFRICAN AMERICAN8 (9.3)6 (6.2)9 (12.5)23 (9.1)
Race, n (%)ASIAN0 (0.0)0 (0.0)0 (0.0)0 (0.0)
Race, n (%)AMERICAN INDIAN OR ALASKA NATIVE0 (0.0)0 (0.0)1 (1.4)1 (0.4)