Skip to contents

Create reactive HTML from a Markdown template. ui_epoxy_markdown() uses the same template syntax as ui_epoxy_html(), but rather than requiring HTML inputs, you can write in markdown. The template is first rendered from markdown to HTML using pandoc::pandoc_convert() (if pandoc is available) or commonmark::markdown_html() otherwise.


  .markdown_fn = NULL,
  .markdown_args = list(),
  .class = NULL,
  .style = NULL,
  .item_tag = "span",
  .item_class = NULL,
  .placeholder = "",
  .sep = "",
  .open = "{{",
  .close = "}}",
  .na = "",
  .null = "",
  .literal = FALSE,
  .trim = FALSE,
  .aria_live = c("polite", "off", "assertive"),
  .aria_atomic = TRUE,
  .class_item = deprecated(),
  .container = deprecated(),
  .container_item = deprecated()



The output id


Unnamed arguments are treated as lines of markdown text, and named arguments are treated as initial values for templated variables.


The function used to convert the markdown to HTML. This function is passed the markdown text as a character vector for the first argument and any additional arguments from the list .markdown_args. By default, we use pandoc::pandoc_convert() if pandoc is available, otherwise we use commonmark::markdown_html().


A list of arguments to pass to commonmark::markdown_html().

.class, .style

Classes and inline style directives added to the <epoxy-html> container into which the elements in ... are placed.

.item_tag, .item_class

The HTML element tag name and classes used to wrap each template variable. By default, each template is wrapped in a <span>.


Default placeholder if a template variable placeholder isn't provided.


[character(1): ‘""’]
Separator used to separate elements.


[character(1): ‘\{’]
The opening delimiter around the template variable or expression. Doubling the full delimiter escapes it.


[character(1): ‘\}’]
The closing delimiter around the template variable or expression. Doubling the full delimiter escapes it.


[character(1): ‘NA’]
Value to replace NA values with. If NULL missing values are propagated, that is an NA result will cause NA output. Otherwise the value is replaced by the value of .na.


[character(1): ‘character()’]
Value to replace NULL values with. If character() whole output is character(). If NULL all NULL values are dropped (as in paste0()). Otherwise the value is replaced by the value of .null.


[boolean(1): ‘FALSE’]
Whether to treat single or double quotes, backticks, and comments as regular characters (vs. as syntactic elements), when parsing the expression string. Setting .literal = TRUE probably only makes sense in combination with a custom .transformer, as is the case with glue_col(). Regard this argument (especially, its name) as experimental.


[logical(1): ‘TRUE’]
Whether to trim the input template with trim() or not.

.aria_live, .aria_atomic

The aria-live and aria-atomic attribute values for the entire template region. By default, with "polite", any updates within the region will be announced via screen readers.

If your template includes changes in lots of disparate areas, it would be better to set "aria-live" = "polite" and "aria-atomic" = "true" on specific regions that should be announced together. Otherwise, the default is to announce the entire region within the ui_epoxy_html() whenever any of the values within change. In other words, set .aria_live = "off" and .aria_atomic = NULL on the ui_epoxy_html() parent item and then set "aria-live" = "polite" and "aria-atomic" = "true" on the parent containers of each region in the app that receives updates. ui_epoxy_html() does targeted updates, changing only the parts of the UI that have changed.


[Deprecated] Deprecated in epoxy v1.0.0, please use .item_class instead.


[Deprecated] Deprecated in epoxy v1.0.0, where the container is now always <epoxy-html>.


[Deprecated] Deprecated in epoxy v1.0.0, please use .item_tag instead.


An HTML object.


if (FALSE) { # rlang::is_installed("shiny") && rlang::is_interactive()

# Shiny epoxy template functions don't support inline transformations,
# so we still have to do some prep work ourselves.
bechdel <- epoxy::bechdel

as_dollars <- scales::label_dollar(
  scale_cut = scales::cut_short_scale()
bechdel$budget <- as_dollars(bechdel$budget)
bechdel$domgross <- as_dollars(bechdel$domgross)

vowels <- c("a", "e", "i", "o", "u")
bechdel$genre  <- paste(
  ifelse(substr(tolower(bechdel$genre), 1, 1) %in% vowels, "an", "a"),

movie_ids <- rlang::set_names(

ui <- fixedPage(
      width = 3,
      selectInput("movie", "Movie", movie_ids),
      width = 9,
        .id = "about_movie",
## {{title}}

**Released:** {{ year }} \\
**Rated:** {{ rated }} \\
**IMDB Rating:** {{ imdb_rating }}

_{{ title }}_ is {{ genre }} film released in {{ year }}.
It was filmed in {{ country }} with a budget of {{ budget }}
and made {{ domgross }} at the box office.
_{{ title }}_ recieved a Bechdel rating of **{{ bechdel_rating }}**
for the following plot:

> {{ plot }}

server <- function(input, output, session) {
  movie <- reactive({
    bechdel[bechdel$imdb_id == input$movie, ]

  output$about_movie <- render_epoxy(.list = movie())
  output$poster <- renderUI(
      src = movie()$poster,
      alt = paste0("Poster for ", movie()$title),
      style = "max-height: 400px; max-width: 100%; margin: 0 auto; display: block;"

if (interactive()) {
  shinyApp(ui, server)
if (FALSE) { # rlang::is_interactive()