R Markdown and Quarto Tables: kable, gt, Captions, Widths and Merged Cells
By MarkdownTables.com editorial teamUpdated
On this page
Which table method should I use in R Markdown or Quarto?
R Markdown (.Rmd) and Quarto (.qmd) both run your R code with knitr and pass the result to Pandoc, so every plain Pandoc Markdown table works in both. What you choose beyond that depends on where the document ends up:
| Method | HTML | PDF (LaTeX) | Word (DOCX) | Merged cells |
|---|---|---|---|---|
| Pipe table (hand-written)Pandoc Markdown, static text | Yes | Yes | Yes | No: Not by default |
| knitr::kable()Data frame to table | Yes | Yes | Yes | No: Plain styling |
| kableExtraStyling on top of kable | Yes | Yes | No: HTML and LaTeX only | Yes: collapse_rows(), add_header_above() |
| gt | Yes | Partly: LaTeX support is more limited | Partly: Depends on the gt version | Yes: tab_spanner(), row groups |
| flextableDesigned for Word and PowerPoint | Yes | Partly: Supported with limitations | Yes | Yes: merge_v(), merge_h(), merge_at() |
| DT, reactableJavaScript widgets | Yes | No: Needs a static fallback | No: Needs a static fallback | No: Not by default |
✅ supported · ⚠️ partly or with a workaround · ❌ not supported · Last verified October 2026
“Partial” means support exists but is narrower or depends on package version. Check the package’s current documentation for your format.
- Static text table: a pipe table. It is readable in the source and works everywhere.
- Table from data, any format:
knitr::kable(). - HTML or PDF with styling: kableExtra or gt.
- Word or PowerPoint: flextable.
- Interactive HTML: DT or reactable.
Pipe tables written by hand
A pipe table in a .Rmd or .qmd file uses the same syntax as everywhere else. Pandoc parses it, so Pandoc's extras apply: a caption line, relative column widths and alignment colons.
| model | train | test |
| :------- | ----: | ---: |
| Baseline | 0.912 | 0.841 |
| Tuned | 0.934 | 0.883 || model | train | test |
|---|---|---|
| Baseline | 0.912 | 0.841 |
| Tuned | 0.934 | 0.883 |
Leave a blank line before it and don’t indent it. Syntax rules: Markdown table syntax.
Open in editor : A pipe table in an .Rmd or .qmd fileIn an R Markdown document the caption goes on a Table: line under the table; in Quarto it is a line starting with a colon:
| Option | Meaning |
|:-------|:--------------|
| `-o` | Output file |
| `-t` | Output format |
Table: Command-line options| Option | Meaning |
|:-------|:-------------------------|
| `-o` | Output file |
| `-t` | Output format |
: Command-line options {#tbl-options tbl-colwidths="[25,75]"}
See @tbl-options.More on Pandoc's table forms, including grid tables with spans, is in the Pandoc Markdown tables guide.
How do I make a table from a data frame with knitr::kable()?
knitr::kable() converts a data frame or matrix to a Markdown, HTML or LaTeX table, depending on the output format, so one line of R works in all three:
perf <- data.frame(
model = c("Baseline", "Tuned", "Ensemble"),
train = c(0.912, 0.934, 0.951),
test = c(0.841, 0.883, 0.897)
)
knitr::kable(
perf,
digits = 2,
align = "lrr",
col.names = c("Model", "Train", "Test"),
caption = "Model accuracy"
)| Argument | What it does | Example |
|---|---|---|
digits | Rounds numeric columns | digits = 2 |
align | Per-column alignment: l, c, r | align = "lcr" |
col.names | Replace header labels | col.names = c("Model", "Train") |
caption | Table caption | caption = "Model accuracy" |
row.names | Show or hide row names | row.names = FALSE |
format.args | Pass to format(), e.g. thousands marks | list(big.mark = ",") |
booktabs | LaTeX rules, for PDF output | booktabs = TRUE |
escape | Escape special characters (default TRUE) | escape = FALSE for raw HTML or LaTeX |
knitr::kable(df, row.names = FALSE) # drop row names
knitr::kable(df, format.args = list(big.mark = ",")) # 1,234,567
knitr::kable(df, booktabs = TRUE) # LaTeX/PDF rules
options(knitr.kable.NA = "") # show NA as blankA bare kable() table is plain on purpose. Styling comes from the packages below or from your CSS and Word template.
How do I add a caption and cross-reference a table?
The syntax depends on the engine and on how the table is made.
| Situation | Caption | Reference |
|---|---|---|
R Markdown, kable() | caption = "…" in kable() | Numbering and references need bookdown: \@ref(tab:chunk-label) |
| R Markdown, pipe table | Table: … line under the table | None built in |
| Quarto, code chunk | #| tbl-cap: "…" | #| label: tbl-name, then @tbl-name |
| Quarto, Markdown table | : caption {#tbl-name} under the table | @tbl-name |
In Quarto the label must start with tbl-. A labeled table gets a number (“Table 1”) and every @tbl-name becomes a link to it:
```{r}
#| label: tbl-perf
#| tbl-cap: "Model accuracy on the train and test sets"
#| tbl-colwidths: [50, 25, 25]
knitr::kable(perf, digits = 2, align = "lrr")
```
As @tbl-perf shows, the tuned model generalises better.To place two tables side by side, use #| layout-ncol: 2 with #| tbl-subcap in the chunk. For numbering in R Markdown, use a bookdown output format and name the chunk; the table is then referenced as tab:chunk-label:
---
output: bookdown::html_document2
---
```{r cars-table, echo=FALSE}
knitr::kable(head(cars), caption = "The first rows of cars")
```
See Table \@ref(tab:cars-table).Column width and table alignment
Quarto. Set relative widths with tbl-colwidths, in percent. It works as a chunk option for code-generated tables and as an attribute on a Markdown table's caption line (see the examples above). For HTML styling beyond that, Quarto 1.3 and later post-process tables to apply Bootstrap classes; the html-table-processing: none option turns that off if you want to own the CSS.
Pandoc pipe tables. If any source line is longer than the column limit (72 characters by default), Pandoc makes the table full width and sets relative widths from the number of dashes in the delimiter row. Shorter lines let the content decide:
| Option | Description |
|--------|----------------------------------------------------------------------|
| `-o` | Output file. The extension decides the format, such as .pdf or .docx. |Package options. kableExtra uses column_spec(1, width = "4cm") and kable_styling(full_width = TRUE); gt uses cols_width(); flextable uses width() (in inches) and autofit(). For alignment on the page, kableExtra's position = "center" or "left" and gt's tab_options(table.align = "left") control the whole table; align in kable controls each column. Related syntax: column width and alignment.
How do I merge cells in an R Markdown table?
Pipe tables cannot merge cells. Each package has its own way, and they differ in which formats they cover:
kableExtra (HTML and PDF)
library(knitr)
library(kableExtra)
sales <- data.frame(
region = c("North", "North", "South", "South"),
quarter = c("Q1", "Q2", "Q1", "Q2"),
revenue = c(120.5, 131.2, 98.7, 104.9)
)
kbl(sales, booktabs = TRUE, align = "llr", digits = 1,
caption = "Revenue by region") |>
kable_styling(bootstrap_options = c("striped", "hover"),
latex_options = c("striped", "hold_position"),
full_width = FALSE, position = "center") |>
column_spec(3, width = "3cm") |>
collapse_rows(columns = 1, valign = "top")collapse_rows(columns = 1) merges consecutive identical values in a column into one tall cell. In LaTeX it needs booktabs = TRUE. For headers that span several columns use add_header_above(); for wide PDF tables add latex_options = "scale_down":
kbl(perf, booktabs = TRUE, digits = 2) |>
add_header_above(c(" " = 1, "Accuracy" = 2)) |>
kable_styling(latex_options = "scale_down") # shrink a wide table to fit the pagegt
gt builds tables from parts: header, spanner labels, row groups and formatting. A spanner is its merged header cell:
library(gt)
perf |>
gt() |>
tab_header(title = "Model accuracy") |>
tab_spanner(label = "Accuracy", columns = c(train, test)) |>
cols_label(model = "Model", train = "Train", test = "Test") |>
fmt_percent(columns = c(train, test), decimals = 1) |>
cols_align(align = "left", columns = model) |>
cols_width(model ~ px(160), c(train, test) ~ px(90))flextable (Word, PowerPoint, HTML)
flextable was designed for Office output, which makes it the practical choice when the deliverable is a .docx:
library(flextable)
ft <- flextable(sales)
ft <- merge_v(ft, j = "region") # merge repeated region cells
ft <- valign(ft, j = "region", valign = "top")
ft <- colformat_double(ft, digits = 1)
ft <- set_caption(ft, caption = "Revenue by region")
ft <- theme_booktabs(ft)
ft <- autofit(ft)
ftmerge_v() merges vertically, merge_h() horizontally, and merge_at(i, j) merges a block. Without any package, a raw HTML table with colspan works in HTML output only; see merged cells in Markdown tables.
Sortable and searchable tables
Sorting needs JavaScript, so it only exists in HTML output. DT::datatable() adds sorting, search and paging; reactable adds sorting, filtering and grouping:
# HTML output only
DT::datatable(perf, rownames = FALSE, filter = "top",
options = list(pageLength = 10))
reactable::reactable(perf, searchable = TRUE, filterable = TRUE)A lighter option is the paged data frame printer, which makes every printed data frame a paged table:
# R Markdown
output:
html_document:
df_print: paged
# Quarto
format:
html:
df-print: pagedFor PDF and Word, widgets can't run. Wrap the widget in a chunk that only renders for HTML, or choose a static table for those formats. Sorting for a static Markdown page is covered in sortable tables.
HTML, PDF and Word: what works where
- HTML. The widest choice: every package works. Styling comes from Bootstrap (R Markdown's default themes and Quarto) plus your CSS.
- PDF. Tables become LaTeX. Use
booktabs = TRUE,longtablefor tables that span pages, andscale_downor a smallerfont_sizefor wide ones. Special characters (%,_,&) are escaped by default; turnescapeoff only for raw LaTeX. - Word. Pipe tables and
kable()become Word tables styled by the reference document. kableExtra isn't supported; use flextable or a gt version that supports Word. - Slides. Keep tables short, and check each package's documentation for PowerPoint and Beamer support before you commit to it.
Common R Markdown table problems
A kable table inside a loop shows nothing
knitr prints the value of each top-level expression in a chunk, but not values created inside a for loop or a function body. Those tables are discarded unless you print them as Markdown:
```{r}
for (g in c("a", "b")) {
knitr::kable(data.frame(group = g, n = 1)) # nothing is printed inside a loop
}
``````{r}
#| results: asis
for (g in c("a", "b")) {
print(knitr::kable(data.frame(group = g, n = 1)))
cat("\n\n")
}
```Other problems
- The table shows as plain text. Pipe tables need a blank line before them and no indentation. Check them in the Markdown table validator or the not-rendering guide.
- A pipe in the data splits the cell. In hand-written tables escape it as
\|. Inkable()output the escaping is done for you. - Missing values print as
NA. Setoptions(knitr.kable.NA = "")or usesub_missing()in gt. - The table runs off the PDF page. Use
scale_down,font_size, orlongtable = TRUEfor length. - HTML or
<br>in a cell looks wrong in PDF. Raw HTML only works in HTML output; build format-specific tables withknitr::is_html_output()andknitr::is_latex_output().
If your table starts life in a spreadsheet, convert it with CSV to Markdown table or Excel to Markdown table and paste the result into the document. Notebook users have the Python equivalent in the Jupyter tables guide.
Frequently asked questions
How do I make a table in R Markdown?
Either write a Markdown pipe table (a header row, a |---|---| delimiter row, then rows) or turn a data frame into one with knitr::kable(df) inside an R chunk. Both work in R Markdown and Quarto. Use kable when the data comes from R.
How do I add a caption to a table in R Markdown?
For a data frame, pass caption = "…" to knitr::kable(). For a hand-written pipe table in R Markdown, add a line Table: Your caption under the table. In Quarto, add : Your caption {#tbl-id} under a Markdown table, or #| tbl-cap: "…" in a code chunk.
How do I cross-reference a table in Quarto?
Give the table a label that starts with tbl- and a caption, then write @tbl-label in the text. In a code chunk use #| label: tbl-perf and #| tbl-cap: "…". For a Markdown table put {#tbl-perf} at the end of the caption line.
How do I set column widths in an R Markdown or Quarto table?
In Quarto use #| tbl-colwidths: [60, 40] or tbl-colwidths="[60,40]" on a Markdown table caption. With kableExtra use column_spec(1, width = "4cm"); with gt use cols_width(); with flextable use width(). Pandoc pipe tables use the delimiter row’s dash counts for relative widths when the source lines are wide.
Can you merge cells in an R Markdown table?
Not in a pipe table. Use kableExtra (collapse_rows() for repeated labels, add_header_above() for spanning headers), gt (tab_spanner()), or flextable (merge_v(), merge_h()), which also works in Word. A Pandoc grid table can span cells too.
How do I make a sortable table in R Markdown?
Use DT::datatable(df) or reactable::reactable(df) in an HTML document. Both add click-to-sort headers, and DT adds search and paging. They are JavaScript widgets, so they do not work in PDF or Word output; use knitr::kable() there.
Does kableExtra work with Word output?
No. kableExtra produces HTML and LaTeX only. For a styled table in a Word document use flextable, or recent versions of gt, or write the table as plain knitr::kable() and style it with a Word reference document.
Why does my kable table show raw pipes or no table at all?
A kable call inside a loop or function isn’t auto-printed, and a table printed from code needs results: asis. Call print(knitr::kable(df)) with the chunk option results: asis and add blank lines between tables.
Write the pipe table in a grid
Edit rows visually, set alignment per column, and copy Markdown for your .Rmd or .qmd file.