R Markdown Tables: kable, gt, and Manual Tables
September 11, 2026 · 10 min read
R Markdown Tables: kable, gt, and Manual Tables
An r markdown table comes from one of three places. You type a pipe table in the Markdown text, call knitr::kable() on a data frame, or use a styling package such as kableExtra, gt, or flextable. This guide renders one small data frame all four ways and shows which route survives knitting to HTML, PDF, and Word.
The Data Frame Used in Every Example
Every table below shows the same three materials and their thermal conductivity, so you can compare the output rather than the data. Run this chunk once at the top of your .Rmd file.
```{r setup, include=FALSE}
materials <- data.frame(
material = c("Copper", "Aluminium", "Glass"),
k = c(401, 237, 1.05),
sample = c("A", "B", "C")
)
```
include=FALSE runs the code and hides both the source and the output, which is what you want for setup. The R Markdown cheat sheet covers the other chunk options; this post only uses the ones that affect tables.
How Do You Write a Manual Table in R Markdown?
Outside code chunks, an R Markdown document is pandoc Markdown, so a table typed by hand is a pandoc table. The pipe form is the one to learn: a header row, a separator row of dashes with optional colons for alignment, then one row per line.
| Material | k (W/m K) | Sample |
|:----------|-------------:|:------:|
| Copper | 401.00 | A |
| Aluminium | 237.00 | B |
| Glass | 1.05 | C |
Rendered, that gives a left-aligned first column, right-aligned numbers, and a centred sample code. The separator row is mandatory; without it pandoc treats the lines as a paragraph with stray pipes. Pandoc also accepts simple, multiline, and grid tables, documented in the pandoc manual, but pipe tables are the only ones most editors and GitHub understand.
The full pipe syntax, including escaping pipes inside cells and what alignment colons do, lives in the Markdown table guide. Two things are specific to R Markdown. First, tables in r markdown text can take a caption on the line after the table, written as Table: Thermal conductivity or just : Thermal conductivity. Second, a manual table renders identically in HTML, PDF, and Word because pandoc converts it for each format, which none of the package routes can promise.
Hand-writing is the right choice for a table of static text: a glossary, a list of parameters, a schedule. It's also the fastest fix when a package table misbehaves in one output format, because you can paste the rendered values in and move on. For anything that comes from live data and has to update when the data does, keep reading.
How Does knitr::kable() Build a Table from a Data Frame?
kable() takes a data frame and writes the Markdown (or LaTeX, or HTML) for it. In a knitr document the format argument is detected from the output type, so the same chunk works for every target.
```{r conductivity, echo=FALSE}
knitr::kable(
materials,
digits = 1,
col.names = c("Material", "k (W/m K)", "Sample"),
align = "lrc",
caption = "Thermal conductivity at room temperature"
)
```
Those are the five arguments people actually use. digits rounds numeric columns (one value for all columns or a vector per column). col.names replaces the data frame names with display names. align is a string of l, c, and r characters, one per column; by default numbers are right-aligned and everything else left. caption adds a numbered caption in PDF and Word and a caption element in HTML. The kable reference lists the rest, including row.names = FALSE to drop the row index and format.args = list(big.mark = ",") for thousands separators.
When the output is HTML or Word, kable emits a pipe table much like the manual one above, and pandoc does the rest. When the output is PDF, kable writes LaTeX tabular code directly. That's why kable tables look plain but never break: there's no styling layer to go wrong.
The chunk label is worth setting too. In bookdown-based formats, a kable with a caption in a chunk labelled conductivity can be cross-referenced from the text as Table \@ref(tab:conductivity), and the number updates when tables move. Plain rmarkdown::html_document numbers nothing, so the caption text has to carry any number you want readers to see.
One chunk option matters here. A single kable() call as the last expression in a chunk prints correctly on its own. If you build tables inside a loop or a function, wrap each in print() and set results='asis' on the chunk. Otherwise knitr wraps the Markdown in a code block and you see raw pipes in the output.
Try the Manual R Markdown Table in a Plain Editor
The pipe table is ordinary Markdown. You can draft it, check the alignment, and preview it anywhere before it goes into the .Rmd. The editor below has the manual version; the caption line is pandoc-specific and shows as text here, which is the expected behaviour outside R Markdown.
If the data starts life in a spreadsheet, the CSV to Markdown table converter produces this syntax in one paste, and the Markdown table generator builds it cell by cell.
Styled Tables with kableExtra and gt
Plain kable output is fine for a lab report and wrong for a slide or a client deliverable. Two packages add the styling; they differ in which output formats they cover. Both are on CRAN, so install.packages(c("kableExtra", "gt")) gets you set up, and both print their tables in the RStudio viewer so you can iterate before knitting.
kableExtra extends a kable() object with a pipe chain. It supports HTML and LaTeX, and its functions are format-aware: kable_styling() accepts Bootstrap options such as striped and hover for HTML, and LaTeX options such as hold_position for PDF.
```{r echo=FALSE}
library(kableExtra)
kable(materials, digits = 1, caption = "Thermal conductivity") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
row_spec(0, bold = TRUE)
```
gt builds the table from scratch instead of wrapping kable. Its own site says it supports HTML, LaTeX, and RTF output. The API reads like ggplot2: start with gt(), then add a header, format columns, and style cells.
```{r echo=FALSE}
library(gt)
materials |>
gt() |>
tab_header(title = "Thermal conductivity", subtitle = "W/m K at 20 C") |>
fmt_number(columns = k, decimals = 1) |>
cols_label(material = "Material", k = "k (W/m K)", sample = "Sample")
```
We prefer gt for HTML reports because the code says what the table means (tab_header, cols_label, fmt_number) rather than how it looks. For PDF, kableExtra has the longer track record; its LaTeX support predates gt's, and its vignettes cover the LaTeX-specific traps such as line breaks inside cells.
Which Table Method Works in HTML, PDF, and Word?
This is the question the long-running Stack Overflow threads in the search results are still asking. Here is the matrix as of September 2026, taken from each package's own documentation.
| Method | HTML | PDF (LaTeX) | Word (.docx) |
|---|---|---|---|
| Manual pipe table | Yes | Yes | Yes |
knitr::kable() | Yes | Yes | Yes, unstyled |
| kableExtra | Yes | Yes | Partial: HTML-only functions are dropped |
| gt | Yes | Yes | Not listed among supported formats |
| flextable | Yes | Yes | Yes, the best option |
Three practical rules fall out of that table. If the document must knit to all three formats, use a manual table or plain kable and accept plain styling. If Word is the primary target, use flextable, which was designed for Word and PowerPoint through the officer package and also renders in HTML and PDF. If you only ever knit to HTML, gt or kableExtra give you the most control.
```{r echo=FALSE}
library(flextable)
flextable(materials) |>
set_header_labels(material = "Material", k = "k (W/m K)", sample = "Sample") |>
colformat_double(j = "k", digits = 1) |>
autofit()
```
Everything above applies to Quarto as well: the chunks, kable, gt, and flextable all work unchanged in a .qmd file. The Quarto Markdown guide covers what Quarto adds on top, such as table cross-references without bookdown.
The acknowledged limitation: none of the packages produce identical output across formats. Column widths, fonts, and caption placement all differ between the HTML and PDF renderers, so check each target before you ship rather than trusting the HTML preview in RStudio.
Common R Markdown Table Mistakes
Seeing raw pipes instead of a table. The chunk printed Markdown as text. Either the table was built inside a loop without results='asis', or a cat() call wrapped it. Return the kable object as the chunk's last expression, or add the chunk option and print() each table.
kableExtra styling vanishes in PDF. Functions such as bootstrap_options only apply to HTML. For LaTeX output use latex_options = c("striped", "hold_position") in kable_styling(), and load the package before calling kable() so it can set the right format.
A manual table renders as one long paragraph. The separator row of dashes is missing, or a row has a different number of pipes. Every row needs the same number of cells, and the separator must have at least three dashes per column.
R Markdown Tables FAQ
Building an r markdown table starts with a decision about the target format. A manual pipe table or plain kable() knits everywhere; kableExtra and gt add styling for HTML and PDF; flextable owns Word. Draft the static tables in the editor to check the alignment, keep the data-driven ones in chunks, and knit to every format you promise before calling the report done.