A collection of recipes used to create the reactable demos

Conditional formatting

Color scales

To add color scales, you can use R’s built-in color utilities (or other color manipulation package):

data <- iris[10:29, ]
orange_pal <- function(x) rgb(colorRamp(c("#ffe4cc", "#ff9500"))(x), maxColorValue = 255)

reactable(
  data,
  columns = list(
    Petal.Length = colDef(style = function(value) {
      normalized <- (value - min(data$Petal.Length)) / (max(data$Petal.Length) - min(data$Petal.Length))
      color <- orange_pal(normalized)
      list(background = color)
    })
  )
)
dimnames <- list(start(nottem)[1]:end(nottem)[1], month.abb)
temps <- matrix(nottem, ncol = 12, byrow = TRUE, dimnames = dimnames)

# ColorBrewer-inspired 3-color scale
BuYlRd <- function(x) rgb(colorRamp(c("#7fb7d7", "#ffffbf", "#fc8d59"))(x), maxColorValue = 255)

reactable(
  temps,
  defaultColDef = colDef(
    style = function(value) {
      if (!is.numeric(value)) return()
      normalized <- (value - min(nottem)) / (max(nottem) - min(nottem))
      color <- BuYlRd(normalized)
      list(background = color)
    },
    format = colFormat(digits = 1),
    minWidth = 50
  ),
  columns = list(
    .rownames = colDef(name = "Year", sortable = TRUE, align = "left")
  ),
  bordered = TRUE
)

Formatting changes

stocks <- data.frame(
  Symbol = c("GOOG", "FB", "AMZN", "NFLX", "TSLA"),
  Price = c(1265.13, 187.89, 1761.33, 276.82, 328.13),
  Change = c(4.14, 1.51, -19.45, 5.32, -12.45)
)

reactable(
  stocks,
  columns = list(
    Change = colDef(
      cell = function(value) {
        if (value >= 0) paste0("+", value) else value
      },
      style = function(value) {
        color <- if (value > 0) {
          "#008000"
        } else if (value < 0) {
          "#e00000"
        }
        list(fontWeight = 600, color = color)
      }
    )
  )
)

Tags and badges

library(htmltools)

orders <- data.frame(
  Order = 2300:2304,
  Created = seq(as.Date("2019-04-01"), by = "day", length.out = 5),
  Customer = sample(rownames(MASS::painters), 5),
  Status = sample(c("Pending", "Paid", "Canceled"), 5, replace = TRUE),
  stringsAsFactors = FALSE
)

reactable(
  orders,
  columns = list(
    Status = colDef(cell = function(value) {
      class <- paste0("tag status-", tolower(value))
      div(class = class, value)
    })
  )
)
.tag {
  display: inline-block;
  padding: 2px 12px;
  border-radius: 15px;
  font-weight: 600;
  font-size: 12px;
}

.status-paid {
  background: hsl(116, 60%, 90%);
  color: hsl(116, 30%, 25%);
}

.status-pending {
  background: hsl(230, 70%, 90%);
  color: hsl(230, 45%, 30%);
}

.status-canceled {
  background: hsl(350, 70%, 90%);
  color: hsl(350, 45%, 30%);
}
library(htmltools)

status_badge <- function(color = "#aaa", width = "9px", height = width) {
  span(style = list(
    display = "inline-block",
    marginRight = "8px",
    width = width,
    height = height,
    backgroundColor = color,
    borderRadius = "50%"
  ))
}

reactable(
  orders,
  columns = list(
    Status = colDef(cell = function(value) {
      color <- switch(
        value,
        Paid = "hsl(214, 45%, 50%)",
        Pending = "hsl(30, 97%, 70%)",
        Canceled = "hsl(3, 69%, 50%)"
      )
      badge <- status_badge(color = color)
      tagList(badge, value)
    })
  )
)

Bar charts

There are many ways to create bar charts using HTML and CSS, but here’s one way inspired by Making Charts with CSS.

library(htmltools)

# Render a bar chart with a label on the left
bar_chart <- function(label, width = "100%", height = "16px", fill = "#00bfc4", background = NULL) {
  bar <- div(style = list(background = fill, width = width, height = height))
  chart <- div(style = list(flexGrow = 1, marginLeft = "8px", background = background), bar)
  div(style = list(display = "flex", alignItems = "center"), label, chart)
}

data <- MASS::Cars93[20:49, c("Make", "MPG.city", "MPG.highway")]

reactable(
  data,
  columns = list(
    MPG.city = colDef(name = "MPG (city)", align = "left", cell = function(value) {
      width <- paste0(value / max(data$MPG.city) * 100, "%")
      bar_chart(value, width = width)
    }),
    MPG.highway = colDef(name = "MPG (highway)", align = "left", cell = function(value) {
      width <- paste0(value / max(data$MPG.highway) * 100, "%")
      bar_chart(value, width = width, fill = "#fc5185", background = "#e1e1e1")
    })
  )
)

Positive and negative values

library(htmltools)

# Render a bar chart with positive and negative values
bar_chart_pos_neg <- function(label, value, max_value = 1, height = "16px",
                              pos_fill = "#005ab5", neg_fill = "#dc3220") {
  neg_chart <- div(style = list(flex = "1 1 0"))
  pos_chart <- div(style = list(flex = "1 1 0"))
  width <- paste0(abs(value / max_value) * 100, "%")

  if (value < 0) {
    bar <- div(style = list(marginLeft = "8px", background = neg_fill, width = width, height = height))
    chart <- div(style = list(display = "flex", alignItems = "center", justifyContent = "flex-end"), label, bar)
    neg_chart <- tagAppendChild(neg_chart, chart)
  } else {
    bar <- div(style = list(marginRight = "8px", background = pos_fill, width = width, height = height))
    chart <- div(style = list(display = "flex", alignItems = "center"), bar, label)
    pos_chart <- tagAppendChild(pos_chart, chart)
  }

  div(style = list(display = "flex"), neg_chart, pos_chart)
}

data <- data.frame(
  company = sprintf("Company%02d", 1:10),
  profit_chg = c(0.2, 0.685, 0.917, 0.284, 0.105, -0.701, -0.528, -0.808, -0.957, -0.11)
)

reactable(
  data,
  bordered = TRUE,
  columns = list(
    company = colDef(name = "Company", minWidth = 100),
    profit_chg = colDef(
      name = "Change in Profit",
      defaultSortOrder = "desc",
      cell = function(value) {
        label <- paste0(round(value * 100), "%")
        bar_chart_pos_neg(label, value)
      },
      align = "center",
      minWidth = 400
    )
  )
)

Background bar charts

Another way to create bar charts is to render them as background images. This example creates bar images using the linear-gradient() CSS function, inspired by an example from the DT package.

# Render a bar chart in the background of the cell
bar_style <- function(width = 1, fill = "#e6e6e6", height = "75%", align = c("left", "right"), color = NULL) {
  align <- match.arg(align)
  if (align == "left") {
    position <- paste0(width * 100, "%")
    image <- sprintf("linear-gradient(90deg, %1$s %2$s, transparent %2$s)", fill, position)
  } else {
    position <- paste0(100 - width * 100, "%")
    image <- sprintf("linear-gradient(90deg, transparent %1$s, %2$s %1$s)", position, fill)
  }
  list(
    backgroundImage = image,
    backgroundSize = paste("100%", height),
    backgroundRepeat = "no-repeat",
    backgroundPosition = "center",
    color = color
  )
}

data <- mtcars[, 1:4]

reactable(
  data,
  columns = list(
    mpg = colDef(
      style = function(value) {
        bar_style(width = value / max(data$mpg), fill = "#2c5e77", color = "#fff")
      },
      align = "left",
      format = colFormat(digits = 1)
    ),
    disp = colDef(
      style = function(value) {
        bar_style(width = value / max(data$disp), fill = "hsl(208, 70%, 90%)")
      }
    ),
    hp = colDef(
      style = function(value) {
        bar_style(width = value / max(data$hp), height = "90%", align = "right")
      }
    )
  ),
  bordered = TRUE
)

Embed images

To embed an image, render an <img> element into the table. Be sure to add alt text for accessibility, even if the image is purely decorative (use a null alt="" attribute in this case).

External image files

library(htmltools)

data <- data.frame(
  Animal = c("beaver", "cow", "wolf", "goat"),
  Body = c(1.35, 465, 36.33, 27.66),
  Brain = c(8.1, 423, 119.5, 115)
)

reactable(
  data,
  columns = list(
    Animal = colDef(cell = function(value) {
      image <- img(src = sprintf("images/%s.png", value), height = "24px", alt = value)
      tagList(
        div(style = list(display = "inline-block", width = "45px"), image),
        value
      )
    }),
    Body = colDef(name = "Body (kg)"),
    Brain = colDef(name = "Brain (g)")
  )
)

If the image file is local, ensure the image can be found from the rendered document:

Inline embedded images

Images can also be embedded into documents as a base64-encoded data URL using knitr::image_uri(). This can be more portable, but is usually only recommended for small image files.

library(htmltools)

data <- data.frame(
  Animal = c("beaver", "cow", "wolf", "goat"),
  Body = c(1.35, 465, 36.33, 27.66),
  Brain = c(8.1, 423, 119.5, 115)
)

reactable(
  data,
  columns = list(
    Animal = colDef(cell = function(value) {
      img_src <- knitr::image_uri(sprintf("images/%s.png", value))
      image <- img(src = img_src, height = "24px", alt = value)
      tagList(
        div(style = list(display = "inline-block", width = "45px"), image),
        value
      )
    })
  )
)

Rating stars

This example uses Font Awesome icons (via Shiny) to render rating stars in a table.

To make the rating star icons accessible, wrap them in a container with the ARIA img role and add alternative text using an aria-label or title attribute. This ensures that the multiple icons are identified as a single image by assistive technologies.

library(htmltools)

rating_stars <- function(rating, max_rating = 5) {
  star_icon <- function(empty = FALSE) {
    tagAppendAttributes(shiny::icon("star"),
      style = paste("color:", if (empty) "#edf0f2" else "orange"),
      "aria-hidden" = "true"
    )
  }
  rounded_rating <- floor(rating + 0.5)  # always round up
  stars <- lapply(seq_len(max_rating), function(i) {
    if (i <= rounded_rating) star_icon() else star_icon(empty = TRUE)
  })
  label <- sprintf("%s out of %s stars", rating, max_rating)
  div(title = label, role = "img", stars)
}

ratings <- data.frame(
  Movie = c("Silent Serpent", "Nowhere to Hyde", "The Ape-Man Goes to Mars", "A Menace in Venice"),
  Rating = c(3.65, 2.35, 4.5, 1.4),
  Votes = c(115, 37, 60, 99)
)

reactable(ratings, columns = list(
  Rating = colDef(cell = function(value) rating_stars(value))
))

Show data from other columns

To access data from another column, get the current row data using the row index argument in an R render function, or cellInfo.row in a JavaScript render function. This example shows both ways.

library(dplyr)
library(htmltools)

data <- starwars %>%
  select(character = name, height, mass, gender, homeworld, species)

R render function

reactable(
  data,
  columns = list(
    character = colDef(
      # Show species under character names
      cell = function(value, index) {
        species <- data$species[index]
        species <- if (!is.na(species)) species else "Unknown"
        div(
          div(style = list(fontWeight = 600), value),
          div(style = list(fontSize = 12), species)
        )
      }
    ),
    species = colDef(show = FALSE)
  ),
  defaultPageSize = 6,
  theme = reactableTheme(
    # Vertically center cells
    cellStyle = list(display = "flex", flexDirection = "column", justifyContent = "center")
  )
)

JavaScript render function

reactable(
  data,
  columns = list(
    character = colDef(
      # Show species under character names
      cell = JS("function(cellInfo) {
        var species = cellInfo.row['species'] || 'Unknown'
        return (
          '<div>' +
          '<div style=\"font-weight: 600\">' + cellInfo.value + '</div>' +
          '<div style=\"font-size: 12px\">' + species + '</div>' +
          '</div>'
        )
      }"),
      html = TRUE
    ),
    species = colDef(show = FALSE)
  ),
  defaultPageSize = 6,
  theme = reactableTheme(
    # Vertically center cells
    cellStyle = list(display = "flex", flexDirection = "column", justifyContent = "center")
  )
)

If the column name contains a period, use bracket notation to access the cellInfo.row object: cellInfo.row['species']

Total rows

library(dplyr)
library(htmltools)

data <- MASS::Cars93[18:47, ] %>%
  select(Manufacturer, Model, Type, Sales = Price)

reactable(
  data,
  defaultPageSize = 5,
  columns = list(
    Manufacturer = colDef(footer = "Total"),
    Sales = colDef(footer = sprintf("$%.2f", sum(data$Sales)))
  ),
  defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)

Dynamic totals

To update the total when filtering the table, calculate the total in a JavaScript render function:

reactable(
  data,
  searchable = TRUE,
  defaultPageSize = 5,
  minRows = 5,
  columns = list(
    Manufacturer = colDef(footer = "Total"),
    Sales = colDef(
      footer = JS("function(colInfo) {
        var total = 0
        colInfo.data.forEach(function(row) {
          total += row[colInfo.column.id]
        })
        return '$' + total.toFixed(2)
      }")
    )
  ),
  defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)

Totals with aggregated rows

reactable(
  data,
  groupBy = "Manufacturer",
  searchable = TRUE,
  columns = list(
    Manufacturer = colDef(footer = "Total"),
    Sales = colDef(
      aggregate = "sum",
      format = colFormat(currency = "USD"),
      footer = JS("function(colInfo) {
        var total = 0
        colInfo.data.forEach(function(row) {
          total += row[colInfo.column.id]
        })
        return '$' + total.toFixed(2)
      }")
    )
  ),
  defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)

Nested tables

To create nested tables, use reactable() in a row details renderer:

library(dplyr)

data <- MASS::Cars93[18:47, ] %>%
  mutate(ID = as.character(18:47), Date = seq(as.Date("2019-01-01"), by = "day", length.out = 30)) %>%
  select(ID, Date, Manufacturer, Model, Type, Price)

sales_by_mfr <- group_by(data, Manufacturer) %>%
  summarize(Quantity = n(), Sales = sum(Price))

reactable(
  sales_by_mfr,
  details = function(index) {
    sales <- filter(data, Manufacturer == sales_by_mfr$Manufacturer[index]) %>% select(-Manufacturer)
    tbl <- reactable(sales, outlined = TRUE, highlight = TRUE, fullWidth = FALSE)
    htmltools::div(style = list(margin = "12px 45px"), tbl)
  },
  onClick = "expand",
  rowStyle = list(cursor = "pointer")
)

Units on first row only

To display a label on the first row only (even when sorting), use a JavaScript render function to add the label when the cell’s viewIndex property is 0.

If the label breaks the alignment of values in the column, realign the values by adding white space to the cells without units. Two ways to do this are shown below.

data <- MASS::Cars93[40:44, c("Make", "Length", "Luggage.room")]

reactable(
  data,
  class = "car-specs",
  columns = list(
    # Align values using white space (and a monospaced font)
    Length = colDef(
      cell = JS("function(cellInfo) {
        var units = cellInfo.viewIndex === 0 ? '\u2033' : ' '
        return cellInfo.value + units
      }"),
      class = "number"
    ),
    # Align values using a fixed-width container for units
    Luggage.room = colDef(
      name = "Luggage Room",
      cell = JS("function(cellInfo) {
        var units = cellInfo.viewIndex === 0 ? ' ft³' : ''
        return cellInfo.value + '<div class=\"units\">' + units + '</div>'
      }"),
      html = TRUE
    )
  )
)
.car-specs .number {
  font-family: "Courier New", Courier, monospace;
  white-space: pre;
}

.car-specs .units {
  display: inline-block;
  width: 18px;
}

Tooltips

To add tooltips to a column header, you can render the header as an <abbr> element with a title attribute:

library(htmltools)
library(dplyr)

data <- as_tibble(mtcars[1:6, ], rownames = "car") %>%
  select(car:hp)

with_tooltip <- function(value, tooltip) {
  tags$abbr(style = "text-decoration: underline; text-decoration-style: dotted; cursor: help",
            title = tooltip, value)
}

reactable(
  data,
  columns = list(
    mpg = colDef(header = with_tooltip("mpg", "Miles per US gallon")),
    cyl = colDef(header = with_tooltip("cyl", "Number of cylinders")),
    disp = colDef(header = with_tooltip("disp", "Displacement (cubic inches)")),
    hp = colDef(header = with_tooltip("hp", "Gross horsepower"))
  )
)

The title attribute is inaccessible to most keyboard, mobile, and screen reader users, however, so creating tooltips like this is generally discouraged.

An alternate method would be to use the tippy package, which provides a JavaScript-based tooltip that supports keyboard, touch, and screen reader use.

library(htmltools)
library(dplyr)
library(tippy)

data <- as_tibble(mtcars[1:6, ], rownames = "car") %>%
  select(car:hp)

# See the ?tippy documentation to learn how to customize tooltips
with_tooltip <- function(value, tooltip, ...) {
  div(style = "text-decoration: underline; text-decoration-style: dotted; cursor: help",
      tippy(value, tooltip, ...))
}

reactable(
  data,
  columns = list(
    mpg = colDef(header = with_tooltip("mpg", "Miles per US gallon")),
    cyl = colDef(header = with_tooltip("cyl", "Number of cylinders"))
  )
)

Highlight cells

data <- MASS::road[11:17, ]

reactable(
  data,
  defaultColDef = colDef(
    style = function(value, index, name) {
      if (is.numeric(value) && value == max(data[[name]])) {
        list(fontWeight = "bold")
      }
    }
  )
)

Highlight columns

reactable(
  iris[1:5, ],
  columns = list(
    Petal.Length = colDef(style = list(background = "rgba(0, 0, 0, 0.03)"))
  )
)

Highlight rows

reactable(
  iris[1:5, ],
  rowStyle = function(index) {
    if (index == 2) list(fontWeight = "bold")
    else if (iris[index, "Petal.Length"] >= 1.5) list(background = "rgba(0, 0, 0, 0.05)")
  }
)

Highlight sorted headers

To style sortable headers on hover, select headers with an aria-sort attribute and :hover pseudo-class in CSS:

reactable(iris[1:5, ], defaultColDef = colDef(headerClass = "sort-header"))
.sort-header[aria-sort]:hover {
  background: rgba(0, 0, 0, 0.03);
}

To style sorted headers, select headers with either an aria-sort="ascending" or aria-sort="descending" attribute:

.sort-header[aria-sort="ascending"],
.sort-header[aria-sort="descending"] {
  background: rgba(0, 0, 0, 0.03);
}

Highlight sorted columns

To style sorted columns, use a JavaScript function to style columns based on the table’s sorted state:

reactable(
  iris[1:5, ],
  defaultSorted = "Sepal.Width",
  defaultColDef = colDef(
    style = JS("function(rowInfo, colInfo, state) {
      // Highlight sorted columns
      for (var i = 0; i < state.sorted.length; i++) {
        if (state.sorted[i].id === colInfo.id) {
          return { background: 'rgba(0, 0, 0, 0.03)' }
        }
      }
    }")
  )
)

Borders between groups of data

To add borders between groups, use an R or JavaScript function to style rows based on the previous or next row’s data. If the table can be sorted, use a JavaScript function to style rows only when the groups are sorted.

library(dplyr)

data <- as_tibble(MASS::painters, rownames = "Painter") %>%
  filter(School %in% c("A", "B", "C")) %>%
  mutate(School = recode(School, A = "Renaissance", B = "Mannerist", C = "Seicento")) %>%
  select(Painter, School, everything()) %>%
  group_by(School) %>%
  slice(1:3)

reactable(
  data,
  defaultSorted = list(School = "asc", Drawing = "desc"),
  borderless = TRUE,
  rowStyle = JS("
    function(rowInfo, state) {
      // Ignore padding rows
      if (!rowInfo) return

      // Add horizontal separators between groups when sorting by school
      var firstSorted = state.sorted[0]
      if (firstSorted && firstSorted.id === 'School') {
        var nextRow = state.pageRows[rowInfo.viewIndex + 1]
        if (nextRow && rowInfo.row['School'] !== nextRow['School']) {
          // Use box-shadow to add a 2px border without taking extra space
          return { boxShadow: 'inset 0 -2px 0 rgba(0, 0, 0, 0.1)' }
        }
      }
    }
  ")
)

Merge cells

You can give the appearance of merged cells by hiding cells based on the previous row’s data. Just like with the example above, you’ll need a JavaScript style function for grouping to work with sorting, filtering, and pagination.

library(dplyr)

data <- as_tibble(MASS::painters, rownames = "Painter") %>%
  filter(School %in% c("A", "B", "C")) %>%
  mutate(School = recode(School, A = "Renaissance", B = "Mannerist", C = "Seicento")) %>%
  select(School, Painter, everything()) %>%
  group_by(School) %>%
  slice(1:3)

reactable(
  data,
  columns = list(
    School = colDef(
      style = JS("function(rowInfo, colInfo, state) {
        var firstSorted = state.sorted[0]
        // Merge cells if unsorted or sorting by school
        if (!firstSorted || firstSorted.id === 'School') {
          var prevRow = state.pageRows[rowInfo.viewIndex - 1]
          if (prevRow && rowInfo.row['School'] === prevRow['School']) {
            return { visibility: 'hidden' }
          }
        }
      }")
    )
  ),
  outlined = TRUE
)

Borders between columns

reactable(
  iris[1:5, ],
  columns = list(
    Sepal.Width = colDef(style = list(borderRight = "1px solid rgba(0, 0, 0, 0.1)")),
    Petal.Width = colDef(style = list(borderRight = "1px solid rgba(0, 0, 0, 0.1)"))
  ),
  borderless = TRUE
)

Style nested rows

To style nested rows, use a JavaScript function to style rows based on their nesting level property:

data <- MASS::Cars93[4:8, c("Type", "Price", "MPG.city", "DriveTrain", "Man.trans.avail")]

reactable(
  data,
  groupBy = "Type",
  columns = list(
    Price = colDef(aggregate = "max"),
    MPG.city = colDef(aggregate = "mean", format = colFormat(digits = 1)),
    DriveTrain = colDef(aggregate = "unique"),
    Man.trans.avail = colDef(aggregate = "frequency")
  ),
  rowStyle = JS("function(rowInfo) {
    if (rowInfo.level > 0) {
      return { background: '#eee', borderLeft: '2px solid #ffa62d' }
    } else {
      return { borderLeft: '2px solid transparent' }
    }
  }"),
  defaultExpanded = TRUE
)

Custom fonts

Tables don’t have a default font, and just inherit the font properties from their parent elements. (This may explain why tables look different in R Markdown documents or Shiny apps vs. standalone pages).

To customize the table font, you can set a font on the page, or on the table itself:

reactable(
  iris[1:5, ],
  style = list(fontFamily = "Work Sans, sans-serif", fontSize = "14px"),
  defaultSorted = "Species"
)
# Add a custom font from Google Fonts
htmltools::tags$link(href = "https://fonts.googleapis.com/css?family=Work+Sans:400,600,700&display=fallback",
                     rel = "stylesheet")

As a side note, the reactable package documentation uses the default system fonts installed on your operating system (also known as a system font stack):

font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

Custom sort indicators

To use a custom sort indicator, hide the sort icon with showSortIcon = FALSE and add your own indicator.

For example, changing the sort indicator to a bar using CSS:

reactable(
  iris[1:5, ],
  showSortIcon = FALSE,
  bordered = TRUE,
  defaultSorted = "Petal.Length",
  defaultColDef = colDef(headerClass = "bar-sort-header")
)
.bar-sort-header[aria-sort="ascending"] {
  box-shadow: inset 0 3px 0 0 rgba(0, 0, 0, 0.6) !important;
}

.bar-sort-header[aria-sort="descending"] {
  box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.6) !important;
}

.bar-sort-header {
  transition: box-shadow 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}