A Review of **knitr** Updates from 2024 to 2026
It’s been a long while since I announced updates in knitr last time, so I’m writing this post to cover notable changes in knitr v1.46–v1.52. For the full changelog, please see the release notes on GitHub. I hope you will find this 3-year review helpful.
knitr 1.46 (2024-04-06)
New chunk option tab.cap
You can now set the table caption in the chunk header instead of inside kable(), e.g.,
```{r tab.cap="Motor Trend car data (first six rows)"}
knitr::kable(head(mtcars))
```
Previously you had to write knitr::kable(head(mtcars), caption = "..."), which is inconvenient when you want to keep chunk options together and the R code clean (#1679).
spin() recognizes # %% and #|
Two more chunk delimiters work in spin() now. For example, a VS Code / Jupyter-style script:
# My report
# %%
plot(cars)
# %% results="hide"
invisible(summary(cars))
And Quarto-style pipe comments:
#| label: fig-cars
#| fig-cap: "Speed vs. distance"
plot(cars)
Both can be spun into a document with knitr::spin("script.R"). The old #- delimiter no longer works; please use #+ or any of the above new delimiters (#2307, #2320).
Chunk hooks accept ...
Before, every chunk hook had to declare arguments before, options, envir, and name explicitly, e.g.,
# old: had to list explicitly
knitr::knit_hooks$set(my_hook = function(before, options, envir, name) {
if (before) cat("starting\n")
})
Now ... can be used to capture all and knitr will pass all arguments implicitly to ... (if your hook doesn’t support any of these arguments, they will be silently dropped):
knitr::knit_hooks$set(my_hook = function(before, ...) {
if (before) cat("starting\n")
})
PNG optimization for package vignettes
If optipng or pngquant is on your PATH, vignette PNG plots are automatically compressed during R CMD build. No code change needed—just install the tools. PNG plots on CRAN are often surprisingly large; for vignettes with several plots this can meaningfully reduce the installed package size.
Breaking: unbalanced chunk fences are errors
Any chunk header/footer mismatch now throws an error, e.g.,
````{r}
1 + 1
```
The opening fence has four backticks; the closing fence must match exactly (#2306).
Breaking: vignettes tangled during R CMD check
knitr no longer skips tangling vignettes during R CMD check. The tangled R script is then run, so check failures can now come from the vignette code rather than the vignette build. CRAN sets _R_CHECK_VIGNETTES_SKIP_RUN_MAYBE_=true so this mostly affects non-CRAN checks (e.g., GitHub Actions). Add that environment variable to your workflow if you want the old behavior.
knitr 1.47 (2024-05-29)
Limit kable() rows via global option
Large data frames accidentally passed to kable() can generate enormous tables. Set a global cap if you want to prevent an accidental huge table, e.g.,
options(knitr.kable.max_rows = 30)
knitr::kable(big_df) # only first 30 rows rendered, rest silently dropped
knitr 1.48 (2024-07-08)
This release contains bug fixes only, to which you don’t need to pay attention.
knitr 1.49 (2024-11-08)
Disable in-chunk references with ref.chunk = FALSE
knitr’s <> syntax for reusing chunk code silently mangles R code that legitimately uses <</>> as delimiters—for example, glue::glue() variants that change the open/close markers:
myglue <- function(..., .envir = parent.frame()) {
glue(..., .open = "<<", .close = ">>", .envir = .envir)
}
x <- "<> <- function(x) { <> }"
Any <<...>> pattern on its own line gets silently removed by knitr, so x ends up with missing lines in the knitted output. Set ref.chunk = FALSE to turn off that substitution for the affected chunk (#2360), e.g.,
```{r ref.chunk=FALSE}
...
x <- "<> <- function(x) { <> }"
```
fig.alt for LaTeX
Alt text for figures now works in LaTeX output, e.g.,
```{r fig.alt="A scatter plot of speed versus stopping distance"}
plot(cars)
```
knitr emits \includegraphics[alt={A scatter plot...}]{...}, which is supported by recent versions of LaTeX and enables accessible PDFs (#2378).
Custom evaluation environment
knit_global() now accepts an environment argument, so you can redirect all chunk evaluation to a custom environment. For example, the recommended pattern saves and restores the old environment:
new_env <- new.env(parent = globalenv())
old_env <- knitr::knit_global(new_env)
on.exit(knitr::knit_global(old_env), add = TRUE)
knitr::knit("report.Rmd") # all chunks evaluated in new_env
This is useful for packages like multiverse that need to execute chunks in separate environments per analysis rather than the shared global environment.
knitr 1.50 (2025-03-16)
Precise line numbers for inline code errors
Previously, an error in an inline expression like `r log(-1)` reported the line range of the surrounding paragraph. Now it points to the exact line, making it much faster to locate the offending expression in a long document (#2387).
Error tracebacks in R CMD build
When a vignette fails to build, the full R traceback is now printed. Before, you only got the error message and had to reproduce the failure interactively to see where it came from (#2390).
kable() column alignment for Org Mode
Alignment specifiers now render correctly in Org Mode output, e.g.,
knitr::kable(head(iris), format = "org", align = c("l","r","r","r","l"))
Previously the alignment was ignored and all columns appeared left-aligned (#2391).
combine_words() and write_bib() moved to xfun
Both functions gained a proper home in xfun (my base for miscellaneous functions), e.g.,
xfun::join_words(c("apples", "oranges", "bananas"))
# "apples, oranges, and bananas"
xfun::pkg_bib(c("knitr", "rmarkdown"), file = "refs.bib")
The knitr versions (combine_words(), write_bib()) remain as wrappers and are not deprecated, but new code should prefer the xfun versions.
knitr 1.51 (2025-12-20)
OpenTelemetry tracing
With otel / otelsdk installed and a tracer configured, knitting automatically emits spans you can send to any OpenTelemetry backend, e.g.,
library(otelsdk)
# configure your exporter, then:
knitr::knit("report.Rmd")
# → spans: "knitr processing", "knitr output", one "knit" span per chunk
Each per-chunk span records the chunk label and engine, so you can profile which chunks are slow in a large document (#2422).
Before/after-knit hooks
You can run setup or teardown code around the entire knit, not just around each chunk, e.g.,
knitr::knit_hooks$set(
before.knit = function() {
con <<- DBI::dbConnect(RSQLite::SQLite(), "data.db")
},
after.knit = function() {
DBI::dbDisconnect(con)
}
)
fig.alt = '' emits empty alt attribute
Setting fig.alt = "" now produces instead of omitting the attribute entirely. Use fig.alt = NA to omit it, e.g.,
# decorative image: alt="" tells screen readers to skip it
knitr::opts_chunk$set(fig.alt = "") # →
# no attribute at all (old behavior for empty alt):
knitr::opts_chunk$set(fig.alt = NA) # → (no alt)
The distinction matters for accessibility audits: alt="" explicitly marks a figure as decorative; a missing alt is flagged as an error by most tools (#2415).
knitr 1.52 (2026-09-06)
New chunk option fig.note
Add a source note or explanatory note below a figure, separate from the caption, e.g.,
```{r fig.cap="US population", fig.note="Source: US Census Bureau"}
plot(uspop)
```
For HTML this becomes
inside the figure , which you can style with CSS. For LaTeX it emits \figurenote{Source: US Census Bureau} inside the figure environment (you can redefine that command in the preamble, e.g., to use floatrow’s \floatfoot). Typst output is also supported (#2022).
Typst support: .Rtyp input and knitr::rtyp vignette engine
I wrote a dedicated post about this feature. In short, you can now write R + Typst documents with a .Rtyp extension and knit them like R Markdown, e.g.,
knitr::knit("report.Rtyp") # → report.typ
knitr::knit2pdf("report.Rtyp") # → report.pdf (via Typst)
I’m not sure how many people would actually use this at all, but at least I’m glad that Frank Harrell has been exploring his exciting new journey with the Typst support in knitr (#2401, #2283).
A new vignette engine means packages can ship Typst vignettes with just two lines in DESCRIPTION:
VignetteBuilder: knitr
Suggests: knitr
and a vignette header:
%\VignetteEngine{knitr::rtyp}
%\VignetteIndexEntry{My Typst Vignette}
(#2447)
hook_plot_tex() respects animation.hook
Previously only hook_plot_html() was extensible via animation.hook / animation.fun. Now the LaTeX hook respects the same option, e.g., to use xmpmulti for Beamer overlays instead of animate:
knitr::opts_chunk$set(
animation.hook = function(x, options) {
# use \multiinclude from xmpmulti for Beamer overlays
paste0("\\multiinclude[format=png]{", xfun::sans_ext(x[1]), "}")
}
)
Without this change, LaTeX animations were locked to the animate package (#2452).
WebP figures via ragg::agg_webp()
For example:
knitr::opts_chunk$set(dev = "agg_webp", fig.ext = "webp")
Requires ragg >= 1.5.0. WebP is typically 25–35% smaller than PNG at equivalent visual quality, useful for HTML reports where file size matters (#2434).
dev.args works with gridSVG
For example:
knitr::opts_chunk$set(
dev = "gridSVG",
dev.args = list(strict = FALSE, pointsize = 12)
)
knitr now routes strict to gridSVG::grid.export() and pointsize to grDevices::svg(), so you no longer need a custom hook just to pass options to gridSVG (#2450, #2451).
Bug fix: captions dropped for multi-figure chunks
When a single chunk produced more than one figure with captions, only the first caption survived because Pandoc treated consecutive images in one paragraph as inline (uncaptioned) images. knitr now inserts a blank line between captioned figures, e.g.,
```{r fig.cap=c("First plot","Second plot")}
plot(cars)
plot(pressure)
```
Both captions now appear correctly in HTML and PDF output (#2032, #1524, #1760).
Bug fix: include_graphics() paths when output dir differs
For example:
# Rendered with rmarkdown::render("report.Rmd", output_dir = "docs/")
knitr::include_graphics("images/fig1.png")
Previously this produced a broken path in the output because knitr resolved the path relative to the input directory, not the output directory. It now uses the output directory communicated by rmarkdown >= 2.32 (#2171).
knit2html() and friends use litedown directly
knit2html(), stitch(), knit_rd(), and the knitr::knitr vignette engine now call litedown::mark() directly instead of going through the markdown package. This is potentially a breaking change. Previously, markdown::mark() silently fixed several common problems in vignette YAML headers before handing off to litedown; now those fixes no longer happen. If your vignette was relying on that silent repair, it will break. Specifically:
-
yes/noin YAML. YAML boolean values must betrue/falsein litedown. If your vignette header contains e.g.toc: yes, change it totoc: true. -
bibliographywithout rbibutils. If your YAML has abibliography:field but the rbibutils package is not installed,markdown::mark()would skip the field silently duringR CMD check. Now you will get an error, so add rbibutils toSuggestsinDESCRIPTIONif you use bibliography in vignettes. - List items with
-in YAML. litedown uses a different YAML syntax than standard YAML for lists. Items starting with-at the beginning of a line are not supported; see the litedown YAML syntax for alternatives.
For packages using the knitr::knitr vignette engine, also update DESCRIPTION to depend on litedown instead of markdown, e.g.,
Suggests: litedown
or if you need the bibliography support:
Suggests: litedown, rbibutils
Acknowledgements
Thanks to all contributors who filed issues or submitted pull requests for these releases: Abhraneel Sarma (@abhsarma), @aksigkvgithub, Arnaud Gallou (@arnaudgallou), @atusy, Sebastian Meyer (@bastistician), @blset, Alex Reinhart (@capnrefsmmat), Christophe Dervieux (@cderv), Patrick R (@codeZeilen), David Cser (@dcser123), Deepayan Sarkar (@deepayan), @DeliciousRoastPotato, @dlampart, David Kaplan (@dmkaplan2000), Chao Cheng (@fenguoerbian), Florian Kohrt (@fkohrt), Floris Vanderhaeghe (@florisvdh), Garrick Aden-Buie (@gadenbuie), @ggrothendieck, Hadley Wickham (@hadley), Watal M. Iwasaki (@heavywatal), Hedvig Skirgård (@HedvigS), Doug Hemken (@Hemken), Michael Higgins (@Higgs32584), Jameel Alsalam (@jameelalsalam), Jennifer Bryan (@jennybc), Jeroen Ooms (@jeroen), Johan Larsson (@jolars), Kevin Ushey (@kevinushey), @knokknok, Kyle F Butts (@kylebutts), Lee Mendelowitz (@LeeMendelowitz), Leonidas Zhak (@LeonidasZhak), L. Sandig (@lsandig), Markus Schlegel (@markschl), Max Schmit (@maxschmi), @mbs2016, @mclements, Matthew Michalska-Smith (@mjsmith037), Nicolás A. Méndez (@naikymen), Nan Xiao (@nanxstats), Olivia Box Power (@Olivia-Box-Power), Ott Toomet (@otoomet), Sebastian Kopf (@sebkopf), @shangeconnew, Charlie Gao (@shikokuchuo), Toby Dylan Hocking (@tdhock), Felix Turbanisch (@turbanisch), Ulrik Lyngs (@ulyngs).
It’s kind of hard to believe that knitr is almost 15 years old now. It has accompanied me for more than 1/3 of my life. Needless to say, this project wouldn’t be possible without the support from the wider R community, especially the 2500+ GitHub issues/PRs these years. If there’s anything else you need, please always feel free to file new issues, comment on/upvote old issues, or send pull requests: https://github.com/yihui/knitr Thank you!