
tabular turns a pre-summarised data frame into a submission-grade clinical table and emits it natively to RTF, PDF, HTML, LaTeX, Typst, and DOCX — no Java, no LibreOffice, no Word automation. One short pipeline gives you decimal alignment via real font metrics, multi-level column headers, predicate-targeted styling, and group-aware pagination, built for CDISC ADaM workflows and FDA / EMA / PMDA submissions.
It is the only R table package that pairs a live HTML preview with a paginated print deliverable: the same spec you eyeball in a notebook is the one that paginates into the RTF you ship.
Scope.
tabularrenders the full set of clinical outputs – tables, listings, and figures (the “T”, “L”, and “F” of TFL) – to RTF, LaTeX, Typst, HTML, PDF, and DOCX from one verb pipeline. A zero-row table renders an empty-data placeholder (“No data available to report”) in the body, with the page chrome and column headers intact.
Install the released version from CRAN:
install.packages("tabular")Or the development version from GitHub:
# install.packages("pak")
pak::pak("vthanik/tabular")
# or
remotes::install_github("vthanik/tabular")R dependencies install automatically. The backends differ in what else they need:
| Backend | Extra requirement |
|---|---|
| RTF, DOCX, HTML, Markdown | none — pure R, no Java, no
pandoc, no Office |
LaTeX (.tex source), Typst (.typ
source) |
none — tabular writes the
source |
| one of two engines: a TeX install (xelatex), or a typst binary — Quarto ≥ 1.4 bundles one, so most machines already qualify |
PDF is the only backend that shells out, and it has two engines:
emit(spec, "out.pdf") compiles
via xelatex when a usable TeX is found.
tabularray + ninecolors ship with
tabular, so no tlmgr_install() step is needed
even on locked-down servers (Domino, Posit Workbench) where
tlmgr install is impossible.emit()
falls back to the typst compiler (the standalone binary, or
the copy bundled inside Quarto, which ships with RStudio / Posit
Workbench). No TeX installation at all, and compiles in well under a
second.Pass format = "latex" or format = "typst"
to pick an engine explicitly; with neither engine present, install
one:
install.packages("tinytex")
tinytex::install_tinytex(bundle = "TinyTeX") # one-time TeX setup
# or: install Quarto (https://quarto.org) — it bundles the typst enginecheck_latex() reports which LaTeX packages resolve
(probed through kpsewhich, the same resolver
xelatex uses) and prints the remedy for anything genuinely
missing; check_typst() does the same for the typst engine
(binary, version floor, and the font chain PDFs render in);
check_fonts(spec) audits the fonts a spec asks for, per
backend.
tabular::check_latex() # LaTeX-PDF readiness, with the install remedy
tabular::check_typst() # Typst-PDF readiness (no TeX needed)TeX Live on a managed OS. If TeX Live came from the system package manager (RHEL
dnf, Debian/Ubuntuapt), itstlmgris usually locked andtlmgr_install()fails on permissions. Install user-space TinyTeX alongside it rather than fighting the system copy — and never reach for--ignore-warningto force it.
The pipeline starts from a pre-summarised wide data frame (one row in
= one display row — tabular does no aggregation) and chains
one verb per concern. Every verb returns an updated, immutable
tabular_spec; the engine resolves it at render time.
library(tabular)
# BigN denominators, keyed by arm
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)
# columns render in data-frame order, so put them in dose order first;
# subset to Age / Sex / Race for a compact display
keep <- c("Age (years)", "Sex, n (%)", "Race, n (%)")
demo <- cdisc_saf_demo[
cdisc_saf_demo$variable %in% keep,
c("variable", "stat_label", "placebo", "drug_50", "drug_100", "Total")
]
tab <- tabular(
demo,
titles = c(
"Table 14.1.1",
"Demographic and Baseline Characteristics",
"Safety Population"
),
footnotes = "Percentages are based on the number of subjects per treatment group."
) |>
cols(
variable = "Characteristic",
stat_label = "Statistic",
placebo = col_spec(
label = "Placebo (N={n['placebo']})",
align = "decimal"
),
drug_50 = col_spec(
label = "Drug 50 (N={n['drug_50']})",
align = "decimal"
),
drug_100 = col_spec(
label = "Drug 100 (N={n['drug_100']})",
align = "decimal"
),
Total = col_spec(label = "Total (N={n['Total']})", align = "decimal")
) |>
group_rows(by = "variable")
# render to any backend by file extension (or format = "...")
path <- emit(tab, tempfile(fileext = ".rtf")) # submission deliverableThe same tab emits to every backend from the one spec.
The table below is tabular’s own HTML render — the identical spec also
produces RTF, a paginated PDF (LaTeX- or typst-compiled), a
tabularray LaTeX fragment, a native Typst document, and
native OOXML .docx:

emit()
dispatches on the file extension to RTF 1.9.1, self-contained Bootstrap
HTML, tabularray LaTeX, native Typst, native OOXML DOCX,
and PDF — compiled through LaTeX when a TeX is installed, or through the
typst engine (bundled with Quarto) on TeX-less machines. No JVM, no
Office round-trip.footnote()
anchors a marker to any cell, header, or title; the engine assigns the
glyph once, in reading order, deduped by id, and
byte-identical across every backend and page.tabular styles
and renders; it never filters, aggregates, or weights. Pair it with
cards / gtsummary / dplyr / SAS
upstream and feed it a tidy wide frame.emit(data_file = ...)
writes the resolved wide data beside the render, and a CDISC ARS audit
manifest documents the display.tabular is a renderer for pre-summarised
clinical tables, not a statistics engine. Compute the summary upstream —
with cards, gtsummary, dplyr, or
SAS — then hand the finished wide frame to tabular(). Reach
for gtsummary or rtables when you want the
package to compute the summary; reach for tabular
to render a summary you already have to submission-grade
output.
The matrix reflects each package’s documented export surface
(verified against their namespaces; via gt means
gtsummary renders through gt):
| tabular | gt | rtables | gtsummary | flextable | huxtable | |
|---|---|---|---|---|---|---|
| Computes statistics | — | — | ✓ | ✓ | — | — |
| Live HTML preview | ✓ | ✓ | — | ✓ | ✓ | ✓ |
| Native RTF | ✓ | ✓ | — | via gt | ✓ | ✓ |
| Native DOCX | ✓ | ✓ | — | via gt | ✓ | ✓ |
| LaTeX | ✓ | ✓ | — | via gt | — | ✓ |
| ✓ | ✓ | ✓ | via gt | — | ✓ | |
| Paginated submission output | ✓ | — | ✓ | — | — | — |
| Decimal align via font metrics | ✓ | — | — | — | — | — |
| CDISC ARS audit manifest | ✓ | — | — | — | — | — |
Two notes on the marks:
knit_print method). rtables
prints a monospace ASCII table by default and ships no
knit_print method, so it is — here; it can
still emit HTML through an explicit as_html() call.pivot_across()MIT © Vignesh Thanikachalam