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.
Arguments
- .spec
The
tabular_specto 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 oneheaders()call. Tip: Passheaders()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().
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 / PT Placebo
N=86 Drug 50
N=96 Drug 100
N=72 Total
N=254 TOTAL SUBJECTS WITH AN EVENT 52 (60.5) 81 (84.4) 66 (91.7) 199 (78.3) SKIN AND SUBCUTANEOUS TISSUE DISORDERS 19 (22.1) 36 (37.5) 35 (48.6) 90 (35.4) PRURITUS 8 (9.3) 21 (21.9) 25 (34.7) 54 (21.3) ERYTHEMA 8 (9.3) 14 (14.6) 14 (19.4) 36 (14.2) RASH 5 (5.8) 13 (13.5) 8 (11.1) 26 (10.2) HYPERHIDROSIS 2 (2.3) 4 (4.2) 8 (11.1) 14 (5.5) SKIN IRRITATION 3 (3.5) 6 (6.2) 5 (6.9) 14 (5.5) GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS 15 (17.4) 36 (37.5) 30 (41.7) 81 (31.9) APPLICATION SITE PRURITUS 6 (7.0) 23 (24.0) 21 (29.2) 50 (19.7) APPLICATION SITE ERYTHEMA 3 (3.5) 13 (13.5) 14 (19.4) 30 (11.8) APPLICATION SITE DERMATITIS 5 (5.8) 9 (9.4) 7 (9.7) 21 (8.3) APPLICATION SITE IRRITATION 3 (3.5) 9 (9.4) 9 (12.5) 21 (8.3) APPLICATION SITE VESICLES 1 (1.2) 5 (5.2) 5 (6.9) 11 (4.3) GASTROINTESTINAL DISORDERS 13 (15.1) 12 (12.5) 17 (23.6) 42 (16.5) DIARRHOEA 9 (10.5) 5 (5.2) 3 (4.2) 17 (6.7) VOMITING 3 (3.5) 4 (4.2) 6 (8.3) 13 (5.1) NAUSEA 3 (3.5) 3 (3.1) 6 (8.3) 12 (4.7) ABDOMINAL PAIN 1 (1.2) 3 (3.1) 1 (1.4) 5 (2.0) SALIVARY HYPERSECRETION 0 (0.0) 0 (0.0) 4 (5.6) 4 (1.6) NERVOUS SYSTEM DISORDERS 6 (7.0) 18 (18.8) 17 (23.6) 41 (16.1) DIZZINESS 2 (2.3) 9 (9.4) 10 (13.9) 21 (8.3) HEADACHE 3 (3.5) 3 (3.1) 5 (6.9) 11 (4.3) SYNCOPE 0 (0.0) 5 (5.2) 2 (2.8) 7 (2.8) SOMNOLENCE 2 (2.3) 3 (3.1) 1 (1.4) 6 (2.4) TRANSIENT ISCHAEMIC ATTACK 0 (0.0) 2 (2.1) 1 (1.4) 3 (1.2) CARDIAC DISORDERS 7 (8.1) 12 (12.5) 14 (19.4) 33 (13.0) SINUS BRADYCARDIA 2 (2.3) 7 (7.3) 8 (11.1) 17 (6.7) MYOCARDIAL INFARCTION 4 (4.7) 2 (2.1) 4 (5.6) 10 (3.9) ATRIAL FIBRILLATION 1 (1.2) 2 (2.1) 2 (2.8) 5 (2.0) SUPRAVENTRICULAR EXTRASYSTOLES 1 (1.2) 1 (1.0) 1 (1.4) 3 (1.2) VENTRICULAR EXTRASYSTOLES 0 (0.0) 2 (2.1) 1 (1.4) 3 (1.2) INFECTIONS AND INFESTATIONS 12 (14.0) 6 (6.2) 11 (15.3) 29 (11.4) NASOPHARYNGITIS 2 (2.3) 4 (4.2) 6 (8.3) 12 (4.7) UPPER RESPIRATORY TRACT INFECTION 6 (7.0) 1 (1.0) 3 (4.2) 10 (3.9) INFLUENZA 1 (1.2) 1 (1.0) 1 (1.4) 3 (1.2) URINARY TRACT INFECTION 2 (2.3) 0 (0.0) 1 (1.4) 3 (1.2) CYSTITIS 1 (1.2) 0 (0.0) 1 (1.4) 2 (0.8) RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS 5 (5.8) 8 (8.3) 9 (12.5) 22 (8.7) COUGH 1 (1.2) 5 (5.2) 5 (6.9) 11 (4.3) NASAL CONGESTION 3 (3.5) 1 (1.0) 3 (4.2) 7 (2.8) DYSPNOEA 1 (1.2) 1 (1.0) 1 (1.4) 3 (1.2) EPISTAXIS 0 (0.0) 1 (1.0) 2 (2.8) 3 (1.2) PHARYNGOLARYNGEAL PAIN 0 (0.0) 1 (1.0) 1 (1.4) 2 (0.8) PSYCHIATRIC DISORDERS 7 (8.1) 9 (9.4) 3 (4.2) 19 (7.5) CONFUSIONAL STATE 2 (2.3) 3 (3.1) 1 (1.4) 6 (2.4) AGITATION 2 (2.3) 3 (3.1) 0 (0.0) 5 (2.0) INSOMNIA 2 (2.3) 0 (0.0) 2 (2.8) 4 (1.6) ANXIETY 0 (0.0) 3 (3.1) 0 (0.0) 3 (1.2) DELUSION 1 (1.2) 0 (0.0) 1 (1.4) 2 (0.8) MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS 3 (3.5) 6 (6.2) 5 (6.9) 14 (5.5) BACK PAIN 1 (1.2) 1 (1.0) 3 (4.2) 5 (2.0) ARTHRALGIA 1 (1.2) 2 (2.1) 1 (1.4) 4 (1.6) SHOULDER PAIN 1 (1.2) 2 (2.1) 0 (0.0) 3 (1.2) MUSCLE SPASMS 0 (0.0) 1 (1.0) 1 (1.4) 2 (0.8) ARTHRITIS 0 (0.0) 0 (0.0) 1 (1.4) 1 (0.4) INVESTIGATIONS 5 (5.8) 4 (4.2) 3 (4.2) 12 (4.7) ELECTROCARDIOGRAM ST SEGMENT DEPRESSION 4 (4.7) 1 (1.0) 0 (0.0) 5 (2.0) ELECTROCARDIOGRAM T WAVE INVERSION 2 (2.3) 1 (1.0) 1 (1.4) 4 (1.6) BLOOD GLUCOSE INCREASED 0 (0.0) 1 (1.0) 1 (1.4) 2 (0.8) ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED 1 (1.2) 1 (1.0) 0 (0.0) 2 (0.8) BIOPSY 0 (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 Control Active Response Placebo
N=86 Drug 50
N=84 Drug 100
N=84 CR 1 (1.2) 1 (1.2) 1 (1.2) PR 1 (1.2) 0 0 SD 1 (1.2) 0 0 NON-CR/NON-PD 0 0 1 (1.2) PD 0 0 1 (1.2) NE 0 1 (1.2) 0 MISSING 83 (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
Variable Treatment Group Parameter Visit Statistic Placebo Drug 50 Drug 100 Diastolic Blood Pressure (mmHg) Baseline n 340 384 288 Diastolic Blood Pressure (mmHg) Baseline Mean (SD) 77.1 (10.7) 76.6 ( 9.8) 78.2 (10.3) Diastolic Blood Pressure (mmHg) Baseline Median 77.7 76.7 78.8 Diastolic Blood Pressure (mmHg) Baseline Min, Max 40 , 110 48 , 108 51 , 108 Diastolic Blood Pressure (mmHg) Week 8 n 292 240 224 Diastolic Blood Pressure (mmHg) Week 8 Mean (SD) 75.2 ( 9.1) 75.4 (10.6) 77.4 ( 9.1) Diastolic Blood Pressure (mmHg) Week 8 Median 76.0 74.0 78.3 Diastolic Blood Pressure (mmHg) Week 8 Min, Max 49 , 101 52 , 100 54 , 98 Diastolic Blood Pressure (mmHg) Week 16 n 272 168 148 Diastolic Blood Pressure (mmHg) Week 16 Mean (SD) 75.1 (10.9) 75.2 (10.0) 76.0 ( 9.0) Diastolic Blood Pressure (mmHg) Week 16 Median 76.0 75.7 77.3 Diastolic Blood Pressure (mmHg) Week 16 Min, Max 49 , 98 55 , 98 50 , 92 Diastolic Blood Pressure (mmHg) End of Treatment n 222 177 168 Diastolic Blood Pressure (mmHg) End of Treatment Mean (SD) 74.4 (10.7) 76.0 (11.2) 76.0 ( 9.9) Diastolic Blood Pressure (mmHg) End of Treatment Median 73.5 76.0 78.0 Diastolic Blood Pressure (mmHg) End of Treatment Min, Max 49 , 104 50 , 100 56 , 98 Pulse Rate (beats/min) Baseline n 340 384 288 Pulse Rate (beats/min) Baseline Mean (SD) 73.5 (11.6) 72.1 (10.8) 72.4 ( 9.7) Pulse Rate (beats/min) Baseline Median 72.3 70.0 71.7 Pulse Rate (beats/min) Baseline Min, Max 51 , 134 50 , 104 52 , 100 Pulse Rate (beats/min) Week 8 n 292 240 224 Pulse Rate (beats/min) Week 8 Mean (SD) 71.8 ( 9.0) 72.6 (11.1) 74.0 ( 8.9) Pulse Rate (beats/min) Week 8 Median 72.0 72.0 73.2 Pulse Rate (beats/min) Week 8 Min, Max 52 , 102 49 , 104 50 , 104 Pulse Rate (beats/min) Week 16 n 272 168 148 Pulse Rate (beats/min) Week 16 Mean (SD) 70.6 ( 8.8) 68.8 ( 9.4) 73.2 ( 9.5) Pulse Rate (beats/min) Week 16 Median 70.2 68.0 72.0 Pulse Rate (beats/min) Week 16 Min, Max 50 , 90 48 , 104 51 , 96 Pulse Rate (beats/min) End of Treatment n 222 177 168 Pulse Rate (beats/min) End of Treatment Mean (SD) 75.2 (11.5) 74.1 ( 9.4) 73.6 ( 9.6) Pulse Rate (beats/min) End of Treatment Median 74.0 75.0 73.0 Pulse Rate (beats/min) End of Treatment Min, Max 51 , 106 50 , 94 50 , 98 Systolic Blood Pressure (mmHg) Baseline n 340 384 288 Systolic Blood Pressure (mmHg) Baseline Mean (SD) 136.8 (17.6) 137.9 (18.5) 137.8 (17.2) Systolic Blood Pressure (mmHg) Baseline Median 136.3 138.0 138.0 Systolic Blood Pressure (mmHg) Baseline Min, Max 80 , 184 100 , 194 100 , 192 Systolic Blood Pressure (mmHg) Week 8 n 292 240 224 Systolic Blood Pressure (mmHg) Week 8 Mean (SD) 136.3 (17.0) 134.9 (17.8) 135.1 (15.5) Systolic Blood Pressure (mmHg) Week 8 Median 136.5 132.3 134.0 Systolic Blood Pressure (mmHg) Week 8 Min, Max 90 , 189 92 , 200 91 , 198 Systolic Blood Pressure (mmHg) Week 16 n 272 168 148 Systolic Blood Pressure (mmHg) Week 16 Mean (SD) 134.6 (18.3) 132.5 (14.3) 133.7 (16.0) Systolic Blood Pressure (mmHg) Week 16 Median 134.0 130.0 132.0 Systolic Blood Pressure (mmHg) Week 16 Min, Max 76 , 190 100 , 168 99 , 186 Systolic Blood Pressure (mmHg) End of Treatment n 222 177 168 Systolic Blood Pressure (mmHg) End of Treatment Mean (SD) 132.7 (15.4) 133.0 (17.1) 132.3 (15.6) Systolic Blood Pressure (mmHg) End of Treatment Median 131.0 130.0 131.0 Systolic Blood Pressure (mmHg) End of Treatment Min, Max 78 , 172 92 , 178 100 , 177 Temperature (C) Baseline n 172 190 144 Temperature (C) Baseline Mean (SD) 36.6 ( 0.4) 36.5 ( 0.4) 36.6 ( 0.4) Temperature (C) Baseline Median 36.7 36.6 36.6 Temperature (C) Baseline Min, Max 35 , 37 35 , 37 36 , 37 Temperature (C) Week 8 n 146 118 112 Temperature (C) Week 8 Mean (SD) 36.6 ( 0.4) 36.6 ( 0.4) 36.6 ( 0.4) Temperature (C) Week 8 Median 36.6 36.7 36.7 Temperature (C) Week 8 Min, Max 36 , 37 36 , 37 36 , 37 Temperature (C) Week 16 n 136 82 74 Temperature (C) Week 16 Mean (SD) 36.7 ( 0.3) 36.6 ( 0.4) 36.6 ( 0.4) Temperature (C) Week 16 Median 36.7 36.6 36.7 Temperature (C) Week 16 Min, Max 36 , 37 36 , 37 36 , 37 Temperature (C) End of Treatment n 74 59 56 Temperature (C) End of Treatment Mean (SD) 36.7 ( 0.4) 36.6 ( 0.4) 36.6 ( 0.4) Temperature (C) End of Treatment Median 36.8 36.7 36.7 Temperature (C) End of Treatment 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(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 Control Active Pooled Drug 50 Drug 100 Characteristic Statistic N=86 N=96 N=72 N=254 Age (years) n 86 96 72 254 Age (years) Mean (SD) 75.2 (8.59) 76.0 (8.11) 73.8 (7.94) 75.1 (8.25) Age (years) Median 76.0 78.0 75.5 77.0 Age (years) Q1, Q3 69.2, 81.8 71.0, 82.0 70.5, 79.0 70.0, 81.0 Age (years) Min, Max 52, 89 51, 88 56, 88 51, 89 Sex, n (%) F 53 (61.6) 55 (57.3) 35 (48.6) 143 (56.3) Sex, n (%) M 33 (38.4) 41 (42.7) 37 (51.4) 111 (43.7) Race, n (%) WHITE 78 (90.7) 90 (93.8) 62 (86.1) 230 (90.6) Race, n (%) BLACK OR AFRICAN AMERICAN 8 (9.3) 6 (6.2) 9 (12.5) 23 (9.1) Race, n (%) ASIAN 0 (0.0) 0 (0.0) 0 (0.0) 0 (0.0) Race, n (%) AMERICAN INDIAN OR ALASKA NATIVE 0 (0.0) 0 (0.0) 1 (1.4) 1 (0.4)