A collection of recipes used to create the reactable demos
data <- data.frame( Address = c("https://google.com", "https://yahoo.com", "https://duckduckgo.com"), Site = c("Google", "Yahoo", "DuckDuckGo") ) reactable(data, columns = list( # Using htmltools to render a link Address = colDef(cell = function(value) { htmltools::tags$a(href = value, target = "_blank", value) }), # Or using raw HTML Site = colDef(html = TRUE, cell = function(value, index) { sprintf('<a href="%s" target="_blank">%s</a>', data$Address[index], value) }) ))
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) # Excel-inspired 3-color scale GnYlRd <- function(x) rgb(colorRamp(c("#63be7b", "#ffeb84", "#f87274"))(x), maxColorValue = 255) reactable( temps, defaultColDef = colDef( style = function(value) { if (!is.numeric(value)) return() normalized <- (value - min(nottem)) / (max(nottem) - min(nottem)) color <- GnYlRd(normalized) list(background = color) }, format = colFormat(digits = 1), minWidth = 50 ), columns = list( .rownames = colDef(name = "Year", sortable = TRUE, align = "left") ), bordered = TRUE )
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) } ) ))
There are different ways to create bar charts, but here’s one way using HTML and CSS:
library(htmltools) data <- MASS::Cars93[20:49, c("Make", "MPG.city", "MPG.highway")] # 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) } 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") }) ))
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 ) ))
To embed an image, render an <img> element into the table. Make sure to add alt text for accessibility, even if the image is purely decorative (use a null alt="" in this case).
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 using a local image file, make sure the image can be found from the rendered document.
In pkgdown, you can add external resource files using the
resource_filesfield: https://pkgdown.r-lib.org/reference/build_articles.html#external-files.In Shiny, you can add external resource files using the
www/directory oraddResourcePath(): https://shiny.rstudio.com/reference/shiny/latest/resourcePaths.html.
Images can also be embedded into the document as a base64-encoded data URL using knitr::image_uri(). This can be more portable, but is not recommended for large images.
This example uses Font Awesome icons (via Shiny) to render rating stars in a table:
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", rating, max_rating) div(title = label, "aria-label" = 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)) ))
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)
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" tagList( 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") ) )
reactable( data, columns = list( character = colDef( # Show species under character names cell = JS("function(cellInfo) { var species = cellInfo.row['species'] || 'Unknown' return ( '<div style=\"font-weight: 600\">' + cellInfo.value + '</div>' + '<div style=\"font-size: 12px\">' + species + '</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.rowobject:cellInfo.row['species']
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")) )
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")) )
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")) )
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") )
To show a label on the first row only (even when sorting), use a JavaScript render function to add the label when the cell’s viewIndex 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 ) ))
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 way would be to use a package like tippy, which provides a JavaScript based tooltip that supports keyboard, touch, and screen reader use.
# Install tippy (version >= 0.1.0 recommended). # remotes::install_github("JohnCoene/tippy") library(htmltools) library(dplyr) library(tippy) data <- as_tibble(mtcars[1:6, ], rownames = "car") %>% select(car:hp) # See the ?tippy documentation for 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")) ) )
To style sortable headers on hover, select headers with an aria-sort attribute and :hover pseudo-class in CSS:
To style sorted headers, select headers with either an aria-sort="ascending" or aria-sort="descending" attribute:
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)' } } } }") ) )
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) { // 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)' } } } } ") )
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 )
To style nested rows, use a JavaScript function to style rows based on their nesting level:
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 )
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")
Side note: these docs use a system font stack:
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
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: