Skip to content

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:

Table methods by output format
MethodHTMLPDF (LaTeX)Word (DOCX)Merged cells
Pipe table (hand-written)Pandoc Markdown, static textYesYesYesNo: Not by default
knitr::kable()Data frame to tableYesYesYesNo: Plain styling
kableExtraStyling on top of kableYesYesNo: HTML and LaTeX onlyYes: collapse_rows(), add_header_above()
gtYesPartly: LaTeX support is more limitedPartly: Depends on the gt versionYes: tab_spanner(), row groups
flextableDesigned for Word and PowerPointYesPartly: Supported with limitationsYesYes: merge_v(), merge_h(), merge_at()
DT, reactableJavaScript widgetsYesNo: Needs a static fallbackNo: Needs a static fallbackNo: 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.

A pipe table in an .Rmd or .qmd file
Markdown
| model    | train | test |
| :------- | ----: | ---: |
| Baseline | 0.912 | 0.841 |
| Tuned    | 0.934 | 0.883 |
Preview
modeltraintest
Baseline0.9120.841
Tuned0.9340.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 file

In an R Markdown document the caption goes on a Table: line under the table; in Quarto it is a line starting with a colon:

Caption on a Pandoc pipe tableR Markdown
| Option | Meaning       |
|:-------|:--------------|
| `-o`   | Output file   |
| `-t`   | Output format |

Table: Command-line options
Caption, label and column widths on a Markdown tableQuarto
| 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:

A formatted table from a data frameR
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"
)
knitr::kable() arguments you will use most
ArgumentWhat it doesExample
digitsRounds numeric columnsdigits = 2
alignPer-column alignment: l, c, ralign = "lcr"
col.namesReplace header labelscol.names = c("Model", "Train")
captionTable captioncaption = "Model accuracy"
row.namesShow or hide row namesrow.names = FALSE
format.argsPass to format(), e.g. thousands markslist(big.mark = ",")
booktabsLaTeX rules, for PDF outputbooktabs = TRUE
escapeEscape special characters (default TRUE)escape = FALSE for raw HTML or LaTeX
Frequently used optionsR
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 blank

A 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.

Captions and cross-references
SituationCaptionReference
R Markdown, kable()caption = "…" in kable()Numbering and references need bookdown: \@ref(tab:chunk-label)
R Markdown, pipe tableTable: … line under the tableNone 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:

A captioned, labeled table from R codeQuarto
```{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:

bookdown table numberingR Markdown
---
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:

Dashes set relative widths when lines are longMarkdown
| 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)

Styling, widths and merged repeated labelsR
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":

A spanning headerR
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 page

gt

gt builds tables from parts: header, spanner labels, row groups and formatting. A spanner is its merged header cell:

A gt table with a spannerR
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:

Merged cells that survive in WordR
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)
ft

merge_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:

Sortable HTML tablesR
# 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:

df_print: paged in R Markdown; df-print: paged in QuartoYAML
# R Markdown
output:
  html_document:
    df_print: paged

# Quarto
format:
  html:
    df-print: paged

For 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, longtable for tables that span pages, and scale_down or a smaller font_size for wide ones. Special characters (%, _, &) are escaped by default; turn escape off 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:

Nothing is printedR
```{r}
for (g in c("a", "b")) {
  knitr::kable(data.frame(group = g, n = 1))   # nothing is printed inside a loop
}
```
Printed as Markdown, with blank lines between tablesR
```{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 \|. In kable() output the escaping is done for you.
  • Missing values print as NA. Set options(knitr.kable.NA = "") or use sub_missing() in gt.
  • The table runs off the PDF page. Use scale_down, font_size, or longtable = TRUE for length.
  • HTML or <br> in a cell looks wrong in PDF. Raw HTML only works in HTML output; build format-specific tables with knitr::is_html_output() and knitr::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.

Open the table generator