The V Lab
AudienceR Markdown beginners
Study time60–90 minutes
PrerequisitesBasic R is helpful

1 Learning goals

This tutorial shows how to create a reproducible report that combines narrative, R code, computed results, plots, and tables. By the end, you should be able to:

  • structure an .Rmd file with YAML, Markdown, and executable code;
  • control code and output with knitr chunk options;
  • render the source to a self-contained HTML document;
  • create clear tables with knitr::kable(); and
  • avoid common reproducibility, caching, and table-formatting problems.

Terminology: knitr is an R package. kable() is a function exported by knitr, not a separate package. The optional kableExtra package adds advanced styling, but it is not required for the examples in this guide.

2 What R Markdown does

An R Markdown document is a plain-text source file. It keeps the explanation and the analysis together, which makes the report easier to reproduce and update.

Rendering pipeline
.Rmd source → knitr executes code and creates Markdown/assets → Pandoc converts them → HTML, Word, PDF, or another output

Two functions in this pipeline have different jobs:

  • knitr::knit() executes chunks and normally creates an intermediate Markdown document.
  • rmarkdown::render() coordinates knitr, Pandoc, and the requested output format to create the final document.

R Markdown is useful for exploratory analyses, course assignments, laboratory reports, dashboards, model summaries, automated recurring reports, and technical documentation.

3 Install once, render many times

Install the required packages from the R console. Do not put package installation in an automatically executed report: rendering should not unexpectedly modify a reader’s R library.

install.packages(c("rmarkdown", "knitr"))

Render from RStudio’s Knit button or from the R console:

rmarkdown::render(
  "r_markdown_guide_en.Rmd",
  output_file = "r_markdown_guide_en.html",
  encoding = "UTF-8"
)

Pandoc is also required. RStudio includes it; otherwise install Pandoc separately and make it available to rmarkdown.

4 Anatomy of an .Rmd file

An R Markdown file normally has three parts.

4.1 1. YAML front matter

The YAML block at the top stores metadata and output settings. Indentation uses spaces, not tabs.

---
title: "Analysis title"
author: "Analyst name"
date: "2026-08-11"
params:
  min_mpg: 20
output:
  html_document:
    toc: true
    number_sections: true
    code_folding: show
    self_contained: true
---

Useful HTML options include a table of contents (toc), numbered sections, syntax highlighting, floating navigation, and collapsible source code. A self-contained HTML file embeds local CSS, JavaScript, and generated figures so the result is easy to share as one file.

4.2 2. Markdown prose

Markdown supplies lightweight formatting:

# Level-one heading
## Level-two heading

**bold text**, *italic text*, and `inline code`

- an unordered item
- another item

1. a numbered item
2. another item

[R Markdown documentation](https://pkgs.rstudio.com/rmarkdown/)

> A block quotation

Inline mathematics: $\bar{x} = \frac{1}{n}\sum_{i=1}^{n}x_i$

Displayed mathematics:
$$
s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})^2
$$

Blank lines matter: they separate paragraphs, lists, and block-level output.

4.3 3. R code chunks and inline R

A chunk starts with three backticks and {r}, and ends with three backticks. Give every executed chunk a unique, meaningful label.


``` r
summary(mtcars$mpg)
```

```
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   10.40   15.43   19.20   20.09   22.80   33.90
```

Inline R inserts a short result directly into a sentence. For example, mtcars has 32 rows, and its mean fuel economy is 20.1 miles per gallon. Inline expressions should be short and should depend on objects created earlier in the document.

5 A small reproducible analysis

All required objects are created inside this document, so the report does not depend on an interactive Global Environment.

cars_data <- data.frame(
  Model = rownames(mtcars),
  mtcars[, c("mpg", "cyl", "hp", "wt")],
  row.names = NULL,
  check.names = FALSE
)

names(cars_data) <- c("Model", "MPG", "Cylinders", "Horsepower", "Weight")
head(cars_data)
Model MPG Cylinders Horsepower Weight
Mazda RX4 21.0 6 110 2.620
Mazda RX4 Wag 21.0 6 110 2.875
Datsun 710 22.8 4 93 2.320
Hornet 4 Drive 21.4 6 110 3.215
Hornet Sportabout 18.7 8 175 3.440
Valiant 18.1 6 105 3.460

The following plot is generated when the document is knitted. fig.cap adds a caption; fig.width controls the graphics device size in inches, whereas out.width controls its displayed size in the HTML page.

plot(
  mtcars$wt,
  mtcars$mpg,
  xlab = "Weight (1000 lb)",
  ylab = "Miles per gallon",
  pch = 19,
  col = "#2c7fb8"
)

weight_fit <- lm(mpg ~ wt, data = mtcars)
abline(weight_fit, col = "#d95f0e", lwd = 2)
Scatter plot of miles per gallon against vehicle weight with a fitted regression line.

Fuel economy generally decreases as vehicle weight increases.

6 knitr in depth

knitr implements the literate-programming layer of R Markdown: it finds code chunks, evaluates them in order, captures text/graphics/messages, and writes the results into the intermediate document.

6.1 Global and local chunk options

The hidden setup chunk at the beginning of this file uses knitr::opts_chunk$set() to define defaults. A local option in a chunk header overrides the global value only for that chunk.

## This result is visible even though this chunk's source is hidden.

The source above was executed because eval remained TRUE; only its display was changed by echo=FALSE.

6.2 Essential chunk options

Frequently used knitr chunk options
Option Purpose Key_point
echo Show or hide source code The code still runs when echo = FALSE
eval Execute or skip the code eval = FALSE shows code without running it
include Include code and all captured output include = FALSE still runs the code but hides everything
results Control text output: markup, asis, hide, or hold results = ‘asis’ treats generated text as document markup
message Show messages produced by code Messages and warnings are different conditions
warning Show warnings produced by code Hiding a warning does not fix its cause
error Choose whether knitting may continue after an error Use error behavior deliberately; do not hide broken analyses
fig.width / fig.height Set graphics-device dimensions, normally in inches This is not the same as the HTML display size
out.width Set the displayed image width in the output Percent values such as ‘90%’ are convenient for HTML
fig.align Align a figure: left, center, or right Applies to the rendered figure
fig.cap Add a figure caption Useful for accessible, explained output
cache Reuse saved results for an unchanged chunk External dependencies need special care

Three commonly confused settings are worth memorizing:

  • echo=FALSE: run the code, hide the source, keep its results.
  • eval=FALSE: show the source, do not run it, therefore produce no result.
  • include=FALSE: run the code, but hide the source and every captured result.

During development, keep messages and warnings visible until you understand them. Suppressing them is a presentation decision, not a repair.

6.3 Generating Markdown with results='asis'

Normally, character output is formatted like console output. results='asis' tells knitr that trusted generated text is already Markdown or HTML.

cat("### A heading generated by R\n\n")

6.3.1 A heading generated by R

cat("This sentence was created during knitting.\n")

This sentence was created during knitting.

Do not pass untrusted user text through raw HTML generation without sanitizing it.

6.4 Figures and external graphics

Base R, ggplot2, and other graphics systems can be captured automatically. For an existing image, knitr::include_graphics() records the file correctly for the selected output format:

knitr::include_graphics(
  "figures/model-diagnostics.png",
  auto_pdf = TRUE
)

Prefer relative project paths. Avoid setwd() inside a report because it makes path behavior harder to reason about.

6.5 Caching slow chunks

Caching can accelerate slow, deterministic calculations:

```{r fit-expensive-model, cache=TRUE, cache.extra=tools::md5sum("data/input.csv")}
model <- expensive_model(read.csv("data/input.csv"))
```

Do not enable caching everywhere. A code or option change invalidates the normal cache, but changes to an external file may not. cache.extra can add a file hash or another version marker to the cache key. Delete stale caches when in doubt.

6.6 Other useful knitr tools

  • knitr::knit() creates the intermediate knitted document.
  • knitr::purl() extracts R code from an .Rmd file into an .R script.
  • knitr::include_graphics() inserts external graphics safely.
  • knitr::knit_child() composes a report from reusable child documents.
  • knitr::opts_knit controls the knitting process itself; it is distinct from knitr::opts_chunk, which controls chunks.
  • knitr::knit_hooks and knitr::opts_hooks support advanced customization.

knitr can also dispatch chunks to other language engines, but those engines and their runtimes must be installed separately.

7 knitr::kable() in depth

kable() converts rectangular data such as a data frame or matrix into a simple table suited to the current output format. Its deliberately small feature set is an advantage for ordinary reporting: tables remain readable and portable.

7.1 A basic table

cars_table <- cars_data[1:8, ]

knitr::kable(
  cars_table,
  digits = 2,
  row.names = FALSE,
  col.names = c(
    "Model", "MPG", "Cylinders", "Horsepower", "Weight (1000 lb)"
  ),
  align = "lrrrr",
  caption = "Selected variables for the first eight cars"
)
Selected variables for the first eight cars
Model MPG Cylinders Horsepower Weight (1000 lb)
Mazda RX4 21.0 6 110 2.62
Mazda RX4 Wag 21.0 6 110 2.88
Datsun 710 22.8 4 93 2.32
Hornet 4 Drive 21.4 6 110 3.21
Hornet Sportabout 18.7 8 175 3.44
Valiant 18.1 6 105 3.46
Duster 360 14.3 8 245 3.57
Merc 240D 24.4 4 62 3.19

This example illustrates good defaults for report tables:

  • turn off row names unless they carry real information;
  • provide human-readable column labels;
  • left-align text and right-align numbers;
  • round for display without modifying the source data; and
  • add a concise caption.

7.2 The main arguments

Important kable() arguments
Argument What it controls
x A rectangular data frame or matrix (and, in supported cases, a list of them)
format Table syntax such as pipe, simple, html, latex, or rst
digits Rounding digits passed to round(); a vector is recycled across table columns
row.names Whether to display row names
col.names Display labels that replace the input column names
align Column alignment: l, c, or r
caption Text shown with the table
label An identifier whose referencing behavior depends on the output framework
format.args A list passed to format(), for example big.mark and scientific
escape Whether HTML or LaTeX special characters are escaped
… Format-specific settings such as table.attr for HTML

Usually, omit format and let knitr choose it. The pipe and simple formats are portable Markdown; html and latex expose output-specific features but reduce portability.

7.3 A grouped summary table

Create the summary first, then pass a clean rectangular object to kable().

cylinder_values <- sort(unique(mtcars$cyl))

cylinder_summary <- data.frame(
  Cylinders = cylinder_values,
  N = vapply(
    cylinder_values,
    function(value) sum(mtcars$cyl == value),
    numeric(1)
  ),
  Mean_MPG = vapply(
    cylinder_values,
    function(value) mean(mtcars$mpg[mtcars$cyl == value]),
    numeric(1)
  ),
  SD_MPG = vapply(
    cylinder_values,
    function(value) sd(mtcars$mpg[mtcars$cyl == value]),
    numeric(1)
  )
)

knitr::kable(
  cylinder_summary,
  digits = c(0, 0, 2, 2),
  row.names = FALSE,
  col.names = c("Cylinders", "n", "Mean MPG", "SD MPG"),
  align = "rrrr",
  caption = "Fuel economy summarized by cylinder count"
)
Fuel economy summarized by cylinder count
Cylinders n Mean MPG SD MPG
4 11 26.66 4.51
6 7 19.74 1.45
8 14 15.10 2.56

7.4 Column-specific rounding and number formatting

digits controls rounding for display but does not by itself force trailing zeroes. A digits vector is recycled across all table columns, and numeric columns are passed through round(). format.args is passed to base R’s format() and can add thousands separators or avoid scientific notation. Neither argument changes the values stored in the input data frame.

numeric_example <- data.frame(
  Rate = c(0.12345, 0.98765),
  Count = c(1250, 98340),
  Amount = c(15230.4, 824500.8)
)

knitr::kable(
  numeric_example,
  digits = c(3, 0, 2),
  format.args = list(big.mark = ",", scientific = FALSE),
  row.names = FALSE,
  align = "rrr",
  caption = "Column-specific rounding and thousands separators"
)
Column-specific rounding and thousands separators
Rate Count Amount
0.123 1,250 15,230.4
0.988 98,340 824,500.8

7.5 Controlling missing values

By default, missing values are printed as NA. The global option knitr.kable.NA changes their display. Save and restore the old option so later tables are not changed accidentally.

missing_example <- data.frame(
  Student = c("A", "B", "C"),
  Score = c(87, NA, 91)
)

old_options <- options(knitr.kable.NA = "—")

knitr::kable(
  missing_example,
  row.names = FALSE,
  caption = "A custom symbol for a missing value"
)
A custom symbol for a missing value
Student Score
A 87
B —
C 91
options(old_options)

7.6 HTML styling with table.attr

For a document that targets HTML only, format="html" and table.attr can attach CSS classes. This is less portable than a format-neutral table.

knitr::kable(
  head(iris, 8),
  format = "html",
  digits = 1,
  row.names = FALSE,
  table.attr = 'class="table kable-custom"',
  caption = "An HTML table styled with CSS classes"
)
An HTML table styled with CSS classes
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa
4.6 3.4 1.4 0.3 setosa
5.0 3.4 1.5 0.2 setosa

7.7 Escaping: safe by default

With the default escape=TRUE, characters meaningful to HTML or LaTeX are escaped, protecting the document structure. Set escape=FALSE only for trusted, intentionally generated markup.

trusted_summary <- data.frame(
  Metric = c("Mean MPG", "Maximum MPG"),
  Value = c(
    sprintf("<strong>%.1f</strong>", mean(mtcars$mpg)),
    sprintf("<em>%.1f</em>", max(mtcars$mpg))
  ),
  check.names = FALSE
)

knitr::kable(
  trusted_summary,
  format = "html",
  escape = FALSE,
  row.names = FALSE,
  caption = "Trusted, author-generated HTML markup"
)
Trusted, author-generated HTML markup
Metric Value
Mean MPG 20.1
Maximum MPG 33.9

Never use escape=FALSE directly on untrusted or user-submitted text. It can break the page and can create an HTML/script-injection vulnerability.

7.8 Producing several tables in a loop

At the top level, a kable() result is normally printed automatically. Inside a loop or function, print it explicitly. Use results='asis', and separate tables with blank lines or an HTML comment so Pandoc recognizes each block.

for (number_of_cylinders in sort(unique(mtcars$cyl))) {
  subset_rows <- mtcars[
    mtcars$cyl == number_of_cylinders,
    c("mpg", "hp", "wt"),
    drop = FALSE
  ]

  subset_rows <- head(subset_rows, 4)
  subset_rows <- data.frame(
    Model = rownames(subset_rows),
    subset_rows,
    row.names = NULL,
    check.names = FALSE
  )

  print(knitr::kable(
    subset_rows,
    format = "pipe",
    digits = 2,
    row.names = FALSE,
    col.names = c("Model", "MPG", "Horsepower", "Weight"),
    caption = paste("Sample cars with", number_of_cylinders, "cylinders")
  ))

  cat("\n\n<!-- table separator -->\n\n")
}
Sample cars with 4 cylinders
Model MPG Horsepower Weight
Datsun 710 22.8 93 2.32
Merc 240D 24.4 62 3.19
Merc 230 22.8 95 3.15
Fiat 128 32.4 66 2.20
Sample cars with 6 cylinders
Model MPG Horsepower Weight
Mazda RX4 21.0 110 2.62
Mazda RX4 Wag 21.0 110 2.88
Hornet 4 Drive 21.4 110 3.21
Valiant 18.1 105 3.46
Sample cars with 8 cylinders
Model MPG Horsepower Weight
Hornet Sportabout 18.7 175 3.44
Duster 360 14.3 245 3.57
Merc 450SE 16.4 180 4.07
Merc 450SL 17.3 180 3.73

For several tables supplied together, knitr::kable(list(table1, table2)) is a compact option. If each table needs different settings, first create the separate kable() objects and combine them with knitr::kables().

7.9 When kable() is not enough

kable() is intended for simple rectangular tables. Choose another tool when the presentation requires more:

  • kableExtra: spanning headers, grouped rows, footnotes, and detailed HTML/LaTeX styling;
  • DT: interactive HTML tables with sorting, searching, and pagination; or
  • gt, flextable, and similar packages: richer publication formatting.

Captions appear in a normal html_document, but automatic numbering and robust cross-references require a supporting output framework such as bookdown. Raw HTML table markup will not translate directly to Word or PDF.

8 Parameterized reports

The YAML header of this guide defines min_mpg under params. In R code it is available as params$min_mpg, so one source can produce reports for different thresholds, regions, dates, or clients.

selected_cars <- subset(cars_data, MPG >= params$min_mpg)

knitr::kable(
  selected_cars,
  digits = 2,
  row.names = FALSE,
  caption = paste0(
    "Cars with fuel economy of at least ",
    params$min_mpg,
    " MPG"
  )
)
Cars with fuel economy of at least 20 MPG
Model MPG Cylinders Horsepower Weight
Mazda RX4 21.0 6 110 2.62
Mazda RX4 Wag 21.0 6 110 2.88
Datsun 710 22.8 4 93 2.32
Hornet 4 Drive 21.4 6 110 3.21
Merc 240D 24.4 4 62 3.19
Merc 230 22.8 4 95 3.15
Fiat 128 32.4 4 66 2.20
Honda Civic 30.4 4 52 1.61
Toyota Corolla 33.9 4 65 1.83
Toyota Corona 21.5 4 97 2.46
Fiat X1-9 27.3 4 66 1.94
Porsche 914-2 26.0 4 91 2.14
Lotus Europa 30.4 4 113 1.51
Volvo 142E 21.4 4 109 2.78

Override the parameter while rendering:

rmarkdown::render(
  "r_markdown_guide_en.Rmd",
  output_file = "r_markdown_guide_en_mpg22.html",
  params = list(min_mpg = 22),
  envir = new.env(parent = globalenv())
)

Using a new environment keeps objects created by the report out of the Global Environment. For the strongest reproducibility check, render from a fresh R session (for example, with Rscript).

9 Output formats and references

One source can target several formats:

output:
  html_document: default
  word_document: default
  pdf_document: default

HTML needs Pandoc; PDF normally also needs a LaTeX distribution. Output-specific HTML or LaTeX should be used only when portability is not required.

For citations, add bibliography: references.bib to YAML and cite an entry with [@citation-key]. Pandoc uses the bibliography metadata while producing the final document.

10 Common problems and practical rules

  1. The Knit button cannot find an object. Create or import every required object inside the document and in execution order.
  2. YAML fails to parse. Use spaces rather than tabs and align nested keys.
  3. A duplicate-label error appears. Give every chunk a unique label.
  4. A warning is missing from the final report. Temporarily restore warning=TRUE and diagnose it; hiding a warning is not a fix.
  5. Paths work interactively but fail while knitting. Use stable project-relative paths and avoid setwd().
  6. A cached result is stale. Invalidate the cache when external data, configuration, or package versions change.
  7. A table gains an unwanted first column. Use row.names=FALSE explicitly.
  8. Column headings do not match. col.names must have the same length as the displayed data columns.
  9. A table in a loop is absent. Use print(kable(...)) with results='asis'.
  10. HTML markup appears as literal text. Keep escape=TRUE unless the markup is trusted and intentional; then use HTML format deliberately.

Render in a clean R session before sharing. A document that succeeds only after manual console work is not yet reproducible.

11 Reproducibility checklist

  • Keep the .Rmd, data, and supporting files in a project.
  • Use relative paths and record package dependencies.
  • Set a random seed for examples involving randomness.
  • Avoid installation and uncontrolled side effects during knitting.
  • Use informative, unique chunk labels.
  • Render from a clean environment.
  • Keep sessionInfo() available when diagnosing version-dependent behavior.
sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
##
## locale:
## [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
##
## time zone: America/Edmonton
## tzcode source: internal
##
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base
##
## loaded via a namespace (and not attached):
##  [1] digest_0.6.39   R6_2.6.1        fastmap_1.2.0   xfun_0.60       cachem_1.1.0
##  [6] knitr_1.51      htmltools_0.5.9 rmarkdown_2.31  lifecycle_1.0.5 cli_3.6.6
## [11] sass_0.4.10     jquerylib_0.1.4 compiler_4.6.1  tools_4.6.1     evaluate_1.0.5
## [16] bslib_0.12.0    yaml_2.3.12     rlang_1.3.0     jsonlite_2.0.0