Basic Usage

To create a data table, use reactable() on a data frame or matrix. The table will be sortable and paginated by default:

Column definitions

Columns can be customized by providing a named list of column definitions created by colDef() to columns:

reactable(iris[1:5, ], columns = list(
  Sepal.Length = colDef(name = "Sepal Length"),
  Sepal.Width = colDef(name = "Sepal Width"),
  Species = colDef(align = "center")
))

For convenience, you can also specify a default colDef() to use for all columns in defaultColDef:

reactable(
  iris[1:5, ],
  defaultColDef = colDef(
    header = function(value) gsub(".", " ", value, fixed = TRUE),
    cell = function(value) format(value, nsmall = 1),
    align = "center",
    minWidth = 70,
    headerStyle = list(background = "#f7f7f8")
  ),
  columns = list(
    Species = colDef(minWidth = 140)  # overrides the default
  ),
  bordered = TRUE,
  highlight = TRUE
)

Sorting

Tables are sortable by default. You can sort on a column by clicking on its header, or sort on multiple columns by holding the shift key while sorting.

Default sort order

Tables are sorted in ascending order first by default. To customize the sort order, set defaultSortOrder on a table or column to either "asc" (ascending) or "desc" (descending):

reactable(
  iris[48:52, ],
  defaultSortOrder = "desc",
  columns = list(
    Species = colDef(defaultSortOrder = "asc")
  )
)

Default sorted columns

You can pre-sort columns by specifying a vector of column names in defaultSorted:

reactable(iris[48:52, ], defaultSorted = c("Species", "Petal.Length"))

You can also provide a named list to customize the sort orders:

reactable(iris[48:52, ], defaultSorted = list(Species = "asc", Petal.Length = "desc"))

Sort missing values last

You can ignore missing values when sorting by setting sortNALast on a column:

reactable(
  data.frame(
    n = c(1, 2, 3, -Inf, Inf),
    x = c(2, 3, 1, NA, NaN),
    y = c("aa", "cc", "bb", NA, NA)
  ),
  defaultColDef = colDef(sortNALast = TRUE),
  defaultSorted = "x"
)

No sorting

You can disable sorting by setting sortable to FALSE on the table or column. When only some columns are sortable, it can help to indicate sortable columns using showSortable:

reactable(
  iris[1:5, ],
  sortable = FALSE,
  showSortable = TRUE,
  columns = list(
    Petal.Width = colDef(sortable = TRUE),
    Petal.Length = colDef(sortable = TRUE)
  )
)

Hide sort icons

You can hide sort icons by setting showSortIcon to FALSE. This can be used to show a custom sort indicator, for example.

reactable(iris[1:5, ], showSortIcon = FALSE)

Filtering

You can make columns filterable using filterable:

data <- MASS::Cars93[1:20, c("Manufacturer", "Model", "Type", "AirBags", "Price")]

reactable(data, filterable = TRUE, minRows = 10)

To make specific columns filterable (or not), set filterable in the column definition:

reactable(data, filterable = TRUE, columns = list(
  Price = colDef(filterable = FALSE)
))

Searching

You can make the entire table searchable using searchable:

data <- MASS::Cars93[1:20, c("Manufacturer", "Model", "Type", "AirBags", "Price")]

reactable(data, searchable = TRUE, minRows = 10)

Grouping and Aggregation

You can group rows in a table by specifying one or more columns in groupBy:

data <- MASS::Cars93[10:22, c("Manufacturer", "Model", "Type", "Price", "MPG.city")]

reactable(data, groupBy = "Manufacturer")

When rows are grouped, you can aggregate data in a column using an aggregate function:

data <- MASS::Cars93[14:38, 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")
))

You can use one of the built-in aggregate functions:

colDef(aggregate = "sum")        # Sum of numbers
colDef(aggregate = "mean")       # Mean of numbers
colDef(aggregate = "max")        # Maximum of numbers
colDef(aggregate = "min")        # Minimum of numbers
colDef(aggregate = "median")     # Median of numbers
colDef(aggregate = "count")      # Count of values
colDef(aggregate = "unique")     # Comma-separated list of unique values
colDef(aggregate = "frequency")  # Comma-separated counts of unique values

Or a custom aggregate function in JavaScript:

colDef(
  aggregate = JS("
    function(values, rows) {
      // input:
      //  - values: an array of all values in the group
      //  - rows: an array of row data objects for all rows in the group (optional)
      //
      // output:
      //  - an aggregated value, e.g. a comma-separated list
      return values.join(', ')
    }
  ")
)

Multiple groups

data <- data.frame(
  State = state.name,
  Region = state.region,
  Division = state.division,
  Area = state.area
)

reactable(
  data,
  groupBy = c("Region", "Division"),
  columns = list(
    Division = colDef(aggregate = "unique"),
    Area = colDef(aggregate = "sum", format = colFormat(separators = TRUE))
  ),
  bordered = TRUE
)

Custom aggregate function

Custom aggregate functions are useful when none of the built-in aggregate functions apply, or when you want to aggregate values from multiple columns. For example, when calculating aggregate averages or percentages.

Within a custom aggregate function, you can access the values in the column using the values argument, and the values in other columns using the rows argument:

Here’s an example that calculates an aggregate average price by dividing the the sum of two columns, Price and Units:

library(dplyr)

set.seed(10)

data <- sample_n(MASS::Cars93[23:40, ], 30, replace = TRUE) %>%
  mutate(Price = Price * 3, Units = sample(1:5, 30, replace = TRUE)) %>%
  mutate(Avg.Price = Price / Units) %>%
  select(Model, Manufacturer, Price, Units, Avg.Price)

reactable(
  data,
  groupBy = "Manufacturer",
  columns = list(
    Price = colDef(aggregate = "sum", format = colFormat(currency = "USD")),
    Units = colDef(aggregate = "sum"),
    Avg.Price = colDef(
      # Calculate the aggregate Avg.Price as `sum(Price) / sum(Units)`
      aggregate = JS("function(values, rows) {
        var totalPrice = 0
        var totalUnits = 0
        rows.forEach(function(row) {
          totalPrice += row['Price']
          totalUnits += row['Units']
        })
        return totalPrice / totalUnits
      }"),
      format = colFormat(currency = "USD")
    )
  )
)

Column Formatting

You can format data in a column by providing colFormat() options to format:

data <- data.frame(
  price_USD = c(123456.56, 132, 5650.12),
  price_INR = c(350, 23208.552, 1773156.4),
  number_FR = c(123456.56, 132, 5650.12),
  temp = c(22, NA, 31),
  percent = c(0.9525556, 0.5, 0.112),
  date = as.Date(c("2019-01-02", "2019-03-15", "2019-09-22"))
)

reactable(data, columns = list(
  price_USD = colDef(format = colFormat(prefix = "$", separators = TRUE, digits = 2)),
  price_INR = colDef(format = colFormat(currency = "INR", separators = TRUE, locales = "hi-IN")),
  number_FR = colDef(format = colFormat(locales = "fr-FR")),
  temp = colDef(format = colFormat(suffix = " °C")),
  percent = colDef(format = colFormat(percent = TRUE, digits = 1)),
  date = colDef(format = colFormat(date = TRUE, locales = "en-GB"))
))

By default, numbers, dates, times, and currencies are formatted in the locale of the browser. This means users may see different results, depending on the language preferences for their browser.

To use a specific locale for data formatting, provide a vector of BCP 47 language tags in the locales argument. See here for a list of common BCP 47 language tags.

Date formatting

datetimes <- as.POSIXct(c("2019-01-02 3:22:15", "2019-03-15 09:15:55", "2019-09-22 14:20:00"))
data <- data.frame(
  datetime = datetimes,
  date = datetimes,
  time = datetimes,
  time_24h = datetimes,
  datetime_pt_BR = datetimes
)

reactable(data, columns = list(
  datetime = colDef(format = colFormat(datetime = TRUE)),
  date = colDef(format = colFormat(date = TRUE)),
  time = colDef(format = colFormat(time = TRUE)),
  time_24h = colDef(format = colFormat(time = TRUE, hour12 = FALSE)),
  datetime_pt_BR = colDef(format = colFormat(datetime = TRUE, locales = "pt-BR"))
))

Currency formatting

data <- data.frame(
  USD = c(12.12, 2141.213, 0.42, 1.55, 34414),
  EUR = c(10.68, 1884.27, 0.37, 1.36, 30284.32),
  INR = c(861.07, 152122.48, 29.84, 110, 2444942.63),
  JPY = c(1280, 226144, 44.36, 164, 3634634.61),
  MAD = c(115.78, 20453.94, 4.01, 15, 328739.73)
)

reactable(data, columns = list(
  USD = colDef(
    format = colFormat(currency = "USD", separators = TRUE, locales = "en-US")
  ),
  EUR = colDef(
    format = colFormat(currency = "EUR", separators = TRUE, locales = "de-DE")
  ),
  INR = colDef(
    format = colFormat(currency = "INR", separators = TRUE, locales = "hi-IN")
  ),
  JPY = colDef(
    format = colFormat(currency = "JPY", separators = TRUE, locales = "ja-JP")
  ),
  MAD = colDef(
    format = colFormat(currency = "MAD", separators = TRUE, locales = "ar-MA")
  )
))

Formatting aggregated cells

Column formatters apply to both standard and aggregated cells by default. If you want to format aggregated cells separately, provide a named list of cell and aggregated options:

colDef(
  format = list(
    cell = colFormat(...),       # Standard cells
    aggregated = colFormat(...)  # Aggregated cells
  )
)

For example, only the aggregated States are formatted here:

data <- data.frame(
  States = state.name,
  Region = state.region,
  Area = state.area
)

reactable(data, groupBy = "Region", columns = list(
  States = colDef(
    aggregate = "count",
    format = list(
      aggregated = colFormat(suffix = " states")
    )
  ),
  Area = colDef(
    aggregate = "sum",
    format = colFormat(suffix = " mi²", separators = TRUE)
  )
))

Displaying missing values

Missing values are ignored by formatters and shown as empty cells by default. You can customize their display text by setting na on a column:

reactable(
  data.frame(
    n = c(1, 2, NA, 4, 5),
    x = c(55, 27, NA, NaN, 19),
    y = c(1, NA, 0.25, 0.55, NA)
  ),
  columns = list(
    x = colDef(na = "–", format = colFormat(prefix = "$")),
    y = colDef(na = "NA", format = colFormat(percent = TRUE))
  )
)

Custom data formatting

If none of the built-in formatters apply to your data, you can use a custom cell renderer instead.

Custom Rendering

You can customize how data is displayed using an R or JavaScript function that returns custom content. R render functions support Shiny HTML tags (or htmltools) and htmlwidgets, while JavaScript render functions allow for more dynamic behavior.

You can also render content as HTML using colDef(html = TRUE). Note that all raw HTML is escaped by default.

See Custom Rendering for details on how to use render functions, and the Demo Cookbook for even more examples of custom rendering.

Cell rendering

R render function

data <- MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "AirBags", "Price")]

reactable(data, columns = list(
  Model = colDef(cell = function(value, index) {
    # Render as a link
    url <- sprintf("https://wikipedia.org/wiki/%s_%s", data[index, "Manufacturer"], value)
    htmltools::tags$a(href = url, target = "_blank", as.character(value))
  }),
  AirBags = colDef(cell = function(value) {
    # Render as ✘ or ✓
    if (value == "None") "\u2718" else "\u2713"
  }),
  Price = colDef(cell = function(value) {
    # Render as currency
    paste0("$", format(value * 1000, big.mark = ","))
  })
))

JavaScript render function

data <- MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "AirBags", "Price")]

reactable(data, columns = list(
  Model = colDef(html = TRUE, cell = JS("
    function(cellInfo) {
      // Render as a link
      var url = 'https://wikipedia.org/wiki/' + cellInfo.row['Manufacturer'] + '_' + cellInfo.value
      return '<a href=\"' + url + '\" target=\"_blank\">' + cellInfo.value + '</a>'
    }
  ")),
  AirBags = colDef(cell = JS("
    function(cellInfo) {
      // Render as ✘ or ✓ 
      return cellInfo.value === 'None' ? '\u2718' : '\u2713'
    }
  ")),
  Price = colDef(cell = JS("
    function(cellInfo) {
      // Render as currency
      return '$' + (cellInfo.value * 1000).toLocaleString()
    }
  "))
))

Embedding HTML widgets

library(dplyr)
library(sparkline)

data <- chickwts %>%
  group_by(feed) %>%
  summarise(weight = list(weight)) %>%
  mutate(boxplot = NA, sparkline = NA)

reactable(data, columns = list(
  weight = colDef(cell = function(values) {
    sparkline(values, type = "bar", chartRangeMin = 0, chartRangeMax = max(chickwts$weight))
  }),
  boxplot = colDef(cell = function(value, index) {
    sparkline(data$weight[[index]], type = "box")
  }),
  sparkline = colDef(cell = function(value, index) {
    sparkline(data$weight[[index]])
  })
))

Aggregated cell rendering

library(dplyr)

set.seed(10)

data <- sample_n(tail(MASS::Cars93, 9), 30, replace = TRUE) %>%
  select(Manufacturer, Model, Type, Sales = Price)

reactable(
  data,
  groupBy = "Manufacturer",
  searchable = TRUE,
  columns = list(
    Model = colDef(aggregate = "unique"),
    Type = colDef(
      # Render aggregated value as a comma-separated list of unique values
      aggregated = JS("function(cellInfo) {
        var values = cellInfo.subRows.map(function(row) { return row['Type'] })
        var unique = values.reduce(function(obj, v) { obj[v] = true; return obj }, {})
        return Object.keys(unique).join(', ')
      }")
    ),
    Sales = colDef(
      aggregate = "sum",
      # Render aggregated cell as currency
      aggregated = JS("function(cellInfo) {
        return '$' + cellInfo.value.toFixed(2)
      }")
    )
  )
)

Header rendering

library(htmltools)

reactable(
  iris[1:5, ],
  defaultColDef = colDef(header = function(value) {
    units <- div(style = "color: #999", "cm")
    div(title = value, value, units)
  }),
  columns = list(
    Petal.Width = colDef(name = "Petal Width", html = TRUE, align = "left", header = JS("
      function(colInfo) {
        return colInfo.column.name + '<div style=\"color: #999\">cm</div>'
      }
    ")),
    Species = colDef(header = function(value) {
      tags$a(href = "https://wikipedia.org/wiki/List_of_Iris_species", title = "Iris species", value)
    })
  )
)

Footers

You can add column footers by specifying content to render in footer.

footer can also be an R or JavaScript render function — see Custom Rendering for details on how to use render functions.

R render function

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 = function(values) sprintf("$%.2f", sum(values)))
  ),
  defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)

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"))
)

Embedding HTML widgets

library(sparkline)

reactable(
  iris[1:20, ],
  defaultPageSize = 5,
  bordered = TRUE,
  defaultColDef = colDef(footer = function(values) {
    if (!is.numeric(values)) return()
    sparkline(values, type = "box", width = 100, height = 30)
  })
)

Expandable Row Details

You can make rows expandable with additional content through details, which takes an R or JavaScript render function. See Custom Rendering for details on how to use render functions.

reactable(iris[1:5, ], details = function(index) {
  htmltools::div(
    "Details for row: ", index,
    htmltools::tags$pre(paste(capture.output(iris[index, ]), collapse = "\n"))
  )
})

The details column can be customized by providing a colDef() instead. This can be used to add a column name, render HTML content, or change the column width:

reactable(iris[1:5, ], details = colDef(
  name = "More",
  details = JS("function(rowInfo) {
    return 'Details for row: ' + rowInfo.index +
      '<pre>' + JSON.stringify(rowInfo, null, 2) + '</pre>'
  }"),
  html = TRUE,
  width = 60
))

Nested tables

With R render functions, you can render HTML tags, HTML widgets, and even nested tables:

data <- unique(CO2[, c("Plant", "Type")])

reactable(data, details = function(index) {
  plant_data <- CO2[CO2$Plant == data$Plant[index], ]
  htmltools::div(style = "padding: 16px",
    reactable(plant_data, outlined = TRUE)
  )
})

Conditional row details

R render functions support conditional rendering. If a render function returns NULL, the row won’t be expandable:

reactable(iris[1:5, ], details = function(index) {
  if (index %in% c(3, 5)) {
    reactable(data.frame(x = c(1, 2, 3), y = c("a", "b", "c")), fullWidth = FALSE)
  }
})

Multiple row details

You can add details to individual columns, and even show multiple details for a row:

reactable(iris[1:5, ],
  details = function(index) {
    if (index %in% c(3, 5)) {
      reactable(data.frame(x = c(1, 2, 3), y = c("a", "b", "c")), fullWidth = FALSE)
    }
  },
  columns = list(
    Petal.Length = colDef(details = function(index) {
      paste("Petal.Length: ", iris[index, "Petal.Length"])
    }),
    Sepal.Length = colDef(format = colFormat(digits = 1), details = JS("
      function(rowInfo) {
        return 'Sepal.Length: ' + rowInfo.row['Sepal.Length']
      }
    "))
  )
)

Default expanded rows

New in v0.2.0

You can expand all rows by default by setting defaultExpanded to TRUE:

reactable(
  iris[1:4, ],
  details = function(index) paste("Details for row:", index),
  defaultExpanded = TRUE
)

Conditional Styling

You can conditionally style a table using functions that return inline styles or CSS classes. Just like with custom rendering, style functions can either be in R or JavaScript.

See Conditional Styling for details on how to use style functions, and the Demo Cookbook for even more examples of conditional styling.

Cell styling

reactable(sleep[1:6, ], columns = list(
  extra = colDef(style = function(value) {
    if (value > 0) {
      color <- "#008000"
    } else if (value < 0) {
      color <- "#e00000"
    } else {
      color <- "#777"
    }
    list(color = color, fontWeight = "bold")
  })
))
reactable(sleep[1:6, ], columns = list(
  extra = colDef(style = JS("
    function(rowInfo) {
      var value = rowInfo.row['extra']
      if (value > 0) {
        var color = '#008000'
      } else if (value < 0) {
        var color = '#e00000'
      } else {
        var color = '#777'
      }
      return { color: color, fontWeight: 'bold' }
    }
  "))
))

Row styling

reactable(sleep[1:6, ], 
  rowStyle = function(index) {
    if (sleep[index, "extra"] < -1) {
      list(background = "rgba(0, 0, 0, 0.05)")
    }
  },
  rowClass = function(index) {
    if (sleep[index, "extra"] < -1) {
      "bold"
    }
  }
)
reactable(sleep[1:6, ],
  rowStyle = JS("function(rowInfo) {
    if (rowInfo.row['extra'] < -1) {
      return { background: 'rgba(0, 0, 0, 0.05)' }
    }
  }"),
  rowClass = JS("function(rowInfo) {
    if (rowInfo.row['extra'] < -1) {
      return 'bold'
    }
  }")
)

Table Styling

You can customize table styling using several options, which can all be combined:

Highlight rows on hover

reactable(iris[1:5, ], highlight = TRUE)

Bordered

reactable(iris[1:5, ], bordered = TRUE)

Borderless

reactable(iris[1:5, ], borderless = TRUE)

Outlined

reactable(iris[1:5, ], outlined = TRUE)

Striped

reactable(iris[1:5, ], striped = TRUE)

Bordered + striped + highlighting

reactable(iris[1:5, ], bordered = TRUE, striped = TRUE, highlight = TRUE)

Outlined + borderless

reactable(iris[1:5, ], outlined = TRUE, borderless = TRUE)

Compact

reactable(iris[1:5, ], compact = TRUE)

No text wrapping

Long text is wrapped by default, but you can force text to fit on a single line by setting wrap to FALSE:

data <- aggregate(. ~ Species, iris, toString)

reactable(
  data,
  wrap = FALSE,
  resizable = TRUE,
  bordered = TRUE,
  columns = list(Petal.Length = colDef(name = "Petal Length (cm)", width = 100))
)

Fixed height + sticky header/footer

You can make tables scrollable by setting a fixed height or width. Headers and footers are sticky by default, so they stay in place when scrolling:

reactable(
  iris[1:20, ],
  height = 270,
  striped = TRUE,
  defaultColDef = colDef(
    footer = function(values, name) htmltools::div(name, style = list(fontWeight = 600))
  )
)

Sticky columns

You can freeze columns by applying the position: sticky and appropriate left or right CSS properties to columns. This gets complicated when freezing multiple columns, but reactable will make this easier in a future release.

See Sticky Columns for more details and examples.

sticky_style <- list(position = "sticky", left = 0, background = "#fff", zIndex = 1,
                     borderRight = "1px solid #eee")

reactable(
  MASS::Cars93[1:5, ],
  columns = list(
    Manufacturer = colDef(
      style = sticky_style,
      headerStyle = sticky_style
    )
  ),
  defaultColDef = colDef(minWidth = 150)
)

Column widths

By default, columns have a minimum width of 100px and stretch to fill the table. You can control the width of a column using the following arguments in colDef():

  • minWidth - minimum width of the column in pixels (defaults to 100)
  • maxWidth - maximum width of the column in pixels
  • width - fixed width of the column in pixels (overrides minWidth and maxWidth)

When columns stretch, minWidth also controls the ratio at which columns grow. For example, if a table consists of 3 columns having minWidth = 100 each, the columns will stretch at a ratio of 100:100:100. Each column will take up 1/3 of the table’s width and not shrink below 100px.

Another example: if a table consists of three columns having minimum widths of 200px, 100px, and 100px, the columns will take up 50%, 25%, and 25% of the table’s width respectively:

reactable(
  MASS::Cars93[1:6, c("Make", "Type", "Weight")],
  columns = list(
    Make = colDef(minWidth = 200),   # 50% width, 200px minimum
    Type = colDef(minWidth = 100),   # 25% width, 100px minimum
    Weight = colDef(minWidth = 100)  # 25% width, 100px minimum
  ),
  bordered = TRUE
)

No full width

Tables are full width by default, but you can shrink the table to fit its contents by setting fullWidth to FALSE:

reactable(
  MASS::Cars93[1:5, 1:5],
  fullWidth = FALSE,
  bordered = TRUE,
  defaultColDef = colDef(minWidth = 120)
)

You can also set a maximum or fixed width on the table:

reactable(
  MASS::Cars93[1:5, 1:5],
  bordered = TRUE,
  defaultColDef = colDef(minWidth = 120),
  # Set a maximum width on the table:
  style = list(maxWidth = 650),
  # Or a fixed width:
  width = 650
)

Custom CSS

For more control over styling, you can add custom class names to the table and apply your own CSS:

reactable(
  iris[1:18, ],
  defaultPageSize = 6,
  borderless = TRUE,
  class = "my-tbl",
  defaultColDef = colDef(headerClass = "my-header"),
  columns = list(
    Sepal.Width = colDef(class = "my-col"),
    Petal.Width = colDef(class = "my-col")
  ),
  rowClass = "my-row"
)

In R Markdown documents, you can embed CSS using a css language chunk:

```{css, echo=FALSE}
.my-tbl {
  border: 1px solid rgba(0, 0, 0, 0.1);
}

.my-header {
  border-width: 1px;
}

.my-col {
  border-right: 1px solid rgba(0, 0, 0, 0.05);
}

.my-row:hover {
  background-color: #f5f8ff;
}
```

The examples here embed CSS for demonstration, but it’s often better to put CSS in an external style sheet. You can learn more about adding custom CSS to R Markdown documents here, or to Shiny apps here.

Theming

New in v0.2.0

Themes provide a powerful way to customize table styling that can be reused across tables. You can either set theme variables to change the default styles (e.g., row stripe color), or add your own custom CSS to specific elements of the table.

To apply a theme, provide a reactableTheme() to theme:

reactable(
  iris[1:30, ],
  searchable = TRUE,
  striped = TRUE,
  highlight = TRUE,
  bordered = TRUE,
  theme = reactableTheme(
    borderColor = "#dfe2e5",
    stripedColor = "#f6f8fa",
    highlightColor = "#f0f5f9",
    cellPadding = "8px 12px",
    style = list(fontFamily = "-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif"),
    searchInputStyle = list(width = "100%")
  )
)

Global theme

To set the default theme for all tables, use the global reactable.theme option:

options(reactable.theme = reactableTheme(
  color = "hsl(233, 9%, 87%)",
  backgroundColor = "hsl(233, 9%, 19%)",
  borderColor = "hsl(233, 9%, 22%)",
  stripedColor = "hsl(233, 12%, 22%)",
  highlightColor = "hsl(233, 12%, 24%)",
  inputStyle = list(backgroundColor = "hsl(233, 9%, 25%)"),
  selectStyle = list(backgroundColor = "hsl(233, 9%, 25%)"),
  pageButtonHoverStyle = list(backgroundColor = "hsl(233, 9%, 25%)"),
  pageButtonActiveStyle = list(backgroundColor = "hsl(233, 9%, 28%)")
))

reactable(
  iris[1:30, ],
  filterable = TRUE,
  showPageSizeOptions = TRUE,
  striped = TRUE,
  highlight = TRUE,
  details = function(index) paste("Details for row", index)
)

Nested selectors

You can use nested CSS selectors in theme styles to target the current element, using & as the selector, or other child elements (just like in Sass). This is useful for adding pseudo-classes like &:hover, or adding styles in a certain context like .outer-container &.

For example, to highlight headers when sorting:

reactable(
  iris[1:5, ],
  columns = list(Sepal.Length = colDef(sortable = FALSE)),
  showSortable = TRUE,
  theme = reactableTheme(
    headerStyle = list(
      "&:hover[aria-sort]" = list(background = "hsl(0, 0%, 96%)"),
      "&[aria-sort='ascending'], &[aria-sort='descending']" = list(background = "hsl(0, 0%, 96%)"),
      borderColor = "#555"
    )
  )
)

Or to apply a dark theme when a parent element has a certain class, like .dark:

theme <- reactableTheme(
  style = list(".dark &" = list(color = "#fff", background = "#282a36")),
  cellStyle = list(".dark &" = list(borderColor = "rgba(255, 255, 255, 0.15)")),
  headerStyle = list(".dark &" = list(borderColor = "rgba(255, 255, 255, 0.15)")),
  paginationStyle = list(".dark &" = list(borderColor = "rgba(255, 255, 255, 0.15)")),
  rowHighlightStyle = list(".dark &" = list(background = "rgba(255, 255, 255, 0.04)")),
  pageButtonHoverStyle = list(".dark &" = list(background = "rgba(255, 255, 255, 0.08)")),
  pageButtonActiveStyle = list(".dark &" = list(background = "rgba(255, 255, 255, 0.1)"))
)

tbl <- reactable(iris[1:12, ], highlight = TRUE, defaultPageSize = 6, theme = theme)

# Simple theme toggle button
tags$button(onclick = "document.querySelector('.themeable-tbl').classList.toggle('dark')",
            "Toggle light/dark")

# Start with the dark theme enabled
div(class = "themeable-tbl dark", tbl)

Dynamic theming

Themes can also be functions that return a reactableTheme() for context-specific styling.

For example, to style tables in RStudio R Notebooks only when a dark editor theme is active:

options(reactable.theme = function() {
  theme <- reactableTheme(
    color = "hsl(233, 9%, 85%)",
    backgroundColor = "hsl(233, 9%, 19%)",
    borderColor = "hsl(233, 9%, 22%)",
    stripedColor = "hsl(233, 12%, 22%)",
    highlightColor = "hsl(233, 12%, 24%)",
    inputStyle = list(backgroundColor = "hsl(233, 9%, 25%)"),
    selectStyle = list(backgroundColor = "hsl(233, 9%, 25%)"),
    pageButtonHoverStyle = list(backgroundColor = "hsl(233, 9%, 25%)"),
    pageButtonActiveStyle = list(backgroundColor = "hsl(233, 9%, 28%)")
  )

  if (isTRUE(getOption("rstudio.notebook.executing"))) {
    if (requireNamespace("rstudioapi", quietly = TRUE) && rstudioapi::getThemeInfo()$dark) {
      return(theme)
    }
  }
})

Column Groups

You can create column groups by passing a list of colGroup() definitions to columnGroups:

reactable(
  iris[1:5, ],
  columns = list(
    Sepal.Length = colDef(name = "Length"),
    Sepal.Width = colDef(name = "Width"),
    Petal.Length = colDef(name = "Length"),
    Petal.Width = colDef(name = "Width")
  ),
  columnGroups = list(
    colGroup(name = "Sepal", columns = c("Sepal.Length", "Sepal.Width")),
    colGroup(name = "Petal", columns = c("Petal.Length", "Petal.Width"))
  )
)

Column Resizing

You can make columns resizable by setting resizable to TRUE:

reactable(MASS::Cars93[1:5, ], resizable = TRUE, wrap = FALSE, bordered = TRUE)

Row Names

Row names are shown by default, if present. You can customize the row names column using ".rownames" as the column name:

reactable(USPersonalExpenditure, columns = list(
  .rownames = colDef(name = "Category", sortable = TRUE)
))

If row names haven’t been set explicitly, you can force them to show by setting rownames to TRUE:

reactable(iris[1:5, ], rownames = TRUE)

Cell Click Actions

You can add cell click actions using onClick, which accepts the following values:

  • "expand" to expand the row
  • "select" to select the row
  • a JavaScript function for a custom action, e.g., sending the click event to Shiny

Expand on click

reactable(
  iris[48:52, ],
  groupBy = "Species",
  details = function(index) paste("Details for row:", index),
  onClick = "expand",
  # Give rows a pointer cursor to indicate that they're clickable
  rowStyle = list(cursor = "pointer")
)

Select on click

reactable(iris[1:5, ], selection = "multiple", onClick = "select")

Custom action

reactable(iris[1:5, ], onClick = JS("
  function(rowInfo, colInfo) {
    window.alert('clicked row ' + rowInfo.index + ', column ' + colInfo.id)

    // Send the click event to Shiny, which will be available at input$clicked
    // (Note that the row index starts at 0 in JavaScript, so we add 1)
    if (window.Shiny) {
      Shiny.onInputChange('clicked', { column: colInfo.id, index: rowInfo.index + 1 })
    }
  }
"))

Language Options

New in v0.2.0

You can customize the language in the table by providing a set of reactableLang() options to language:

reactable(
  iris[1:30, ],
  searchable = TRUE,
  paginationType = "simple",
  language = reactableLang(
    searchPlaceholder = "Search...",
    noData = "No entries found",
    pageInfo = "{rowStart} to {rowEnd} of {rows} entries",
    pagePrevious = "\u276e",
    pageNext = "\u276f",

    # Accessible labels for assistive technologies such as screen readers.
    # These are already set by default, but don't forget to update them when
    # changing visible text.
    pagePreviousLabel = "Previous page",
    pageNextLabel = "Next page"
  )
)

Global language options

To set the default language strings for all tables, use the global reactable.language option:

options(reactable.language = reactableLang(
  pageSizeOptions = "\u663e\u793a {rows}",
  pageInfo = "{rowStart} \u81f3 {rowEnd} \u9879\u7ed3\u679c,\u5171 {rows} \u9879",
  pagePrevious = "\u4e0a\u9875",
  pageNext = "\u4e0b\u9875"
))

reactable(iris[1:12, ], defaultPageSize = 4, showPageSizeOptions = TRUE)

Shiny

To use reactable in Shiny apps, use renderReactable() and reactableOutput():

library(shiny)
library(reactable)

ui <- fluidPage(
  titlePanel("reactable example"),
  reactableOutput("table")
)

server <- function(input, output, session) {
  output$table <- renderReactable({
    reactable(iris)
  })
}

shinyApp(ui, server)

Row selection

You can enable row selection by setting selection to "single" for single selection, or "multiple" for multiple selection.

To get the selected rows in Shiny, use getReactableState(). The selected rows are given as a vector of row indices (e.g. c(1, 6, 4)) or NULL if no rows are selected.

library(shiny)
library(reactable)

ui <- fluidPage(
  titlePanel("row selection example"),
  reactableOutput("table"),
  verbatimTextOutput("selected")
)

server <- function(input, output, session) {
  selected <- reactive(getReactableState("table", "selected"))

  output$table <- renderReactable({
    reactable(iris, selection = "multiple", onClick = "select")
  })

  output$selected <- renderPrint({
    print(selected())
  })

  observe({
    print(iris[selected(), ])
  })
}

shinyApp(ui, server)

NOTE: selectionId can also be used to get the selected rows in Shiny, but this will be deprecated in a future release.

Default selected rows

New in v0.2.0

You can preselect rows by specifying a vector of row indices in defaultSelected:

reactable(iris[1:4, ], selection = "multiple", defaultSelected = c(1, 3))

Style selected rows

New in v0.2.0

You can style selected rows using rowSelectedStyle in reactableTheme():

reactable(
  iris[1:4, ],
  selection = "multiple",
  defaultSelected = c(1, 3),
  borderless = TRUE,
  onClick = "select",
  theme = reactableTheme(
    rowSelectedStyle = list(backgroundColor = "#eee", boxShadow = "inset 2px 0 0 0 #ffa62d")
  )
)

Or using a rowStyle or rowClass JavaScript function:

reactable(
  MASS::Cars93[10:22, c("Manufacturer", "Model", "Type", "Price", "MPG.city")],
  groupBy = "Manufacturer",
  selection = "multiple",
  defaultSelected = c(1, 2),
  borderless = TRUE,
  onClick = "select",
  rowStyle = JS("function(rowInfo) {
    if (rowInfo && rowInfo.selected) {
      return { backgroundColor: '#eee', boxShadow: 'inset 2px 0 0 0 #ffa62d' }
    }
  }")
)

Customize the selection column

New in v0.2.0

You can customize the selection column using ".selection" as the column name:

reactable(
  iris[1:4, ],
  selection = "multiple",
  columns = list(
    .selection = colDef(
      width = 80,
      style = list(cursor = "pointer"),
      headerStyle = list(cursor = "pointer")
    )
  ),
  bordered = TRUE
)

Update a reactable instance

New in v0.2.0

You can update the selected rows, expanded rows, current page, or data using updateReactable():

library(shiny)
library(reactable)

data <- MASS::Cars93[, 1:7]

ui <- fluidPage(
  actionButton("select_btn", "Select rows"),
  actionButton("clear_btn", "Clear selection"),
  actionButton("expand_btn", "Expand rows"),
  actionButton("collapse_btn", "Collapse rows"),
  actionButton("page_btn", "Change page"),
  selectInput("filter_type", "Filter type", unique(data$Type), multiple = TRUE),
  reactableOutput("table")
)

server <- function(input, output) {
  output$table <- renderReactable({
    reactable(
      data,
      filterable = TRUE,
      searchable = TRUE,
      selection = "multiple",
      details = function(index) paste("Details for row:", index)
    )
  })

  observeEvent(input$select_btn, {
    # Select rows
    updateReactable("table", selected = c(1, 3, 5))
  })

  observeEvent(input$clear_btn, {
    # Clear row selection
    updateReactable("table", selected = NA)
  })

  observeEvent(input$expand_btn, {
    # Expand all rows
    updateReactable("table", expanded = TRUE)
  })

  observeEvent(input$collapse_btn, {
    # Collapse all rows
    updateReactable("table", expanded = FALSE)
  })

  observeEvent(input$page_btn, {
    # Change current page
    updateReactable("table", page = 3)
  })

  observe({
    # Filter data
    filtered <- if (length(input$filter_type) > 0) {
      data[data$Type %in% input$filter_type, ]
    } else {
      data
    }
    updateReactable("table", data = filtered)
  })
}

shinyApp(ui, server)

Get the state of a reactable instance

New in v0.2.0

You can get the current page, page size, number of pages, or selected rows using getReactableState():

library(shiny)
library(reactable)

ui <- fluidPage(
  actionButton("prev_page_btn", "Previous page"),
  actionButton("next_page_btn", "Next page"),
  reactableOutput("table"),
  verbatimTextOutput("table_state")
)

server <- function(input, output) {
  output$table <- renderReactable({
    reactable(
      iris,
      showPageSizeOptions = TRUE,
      selection = "multiple"
    )
  })

  output$table_state <- renderPrint({
    state <- req(getReactableState("table"))
    print(state)
  })

  observeEvent(input$prev_page_btn, {
    # Change to the previous page
    page <- getReactableState("table", "page")
    if (page > 1) {
      updateReactable("table", page = page - 1)
    }
  })

  observeEvent(input$next_page_btn, {
    # Change to the next page
    state <- getReactableState("table")
    if (state$page < state$pages) {
      updateReactable("table", page = state$page + 1)
    }
  })
}

shinyApp(ui, server)

Cross-Widget Interactions

New in v0.2.0

You can link selection and filtering with other HTML widgets in an R Markdown document or Shiny app using Crosstalk. To get started, install the crosstalk package and wrap your data frame in a crosstalk::SharedData object:

install.packages("crosstalk")

library(crosstalk)

data <- SharedData$new(iris)

Then, pass the shared data to reactable() and any other Crosstalk-compatible HTML widget or filter input:

reactable(data)

filter_slider("sepal_length", "Sepal Length", data, ~Sepal.Length)

For more examples and a list of Crosstalk-compatible widgets, check out the Crosstalk documentation: https://rstudio.github.io/crosstalk/using.html

Filtering

Tables can be filtered by widgets that support Crosstalk’s filtering API, such as Crosstalk’s filter_checkbox(), filter_slider(), and filter_select() inputs:

library(crosstalk)

cars <- MASS::Cars93[1:20, c("Manufacturer", "Model", "Type", "Price")]
data <- SharedData$new(cars)

bscols(
  widths = c(3, 9),
  list(
    filter_checkbox("type", "Type", data, ~Type),
    filter_slider("price", "Price", data, ~Price, width = "100%"),
    filter_select("mfr", "Manufacturer", data, ~Manufacturer)
  ),
  reactable(data, minRows = 10)
)

Linked selection

Table selection state can be linked with other widgets that support Crosstalk’s linked selection (or linked brushing) API.

In this example, you can select rows to highlight points on the map, or select areas on the map to highlight rows in the table.

library(crosstalk)
library(leaflet)
library(dplyr)

# A SpatialPointsDataFrame for the map.
# Set a group name to share data points with the table.
brew_sp <- SharedData$new(breweries91, group = "breweries")

# A regular data frame (without coordinates) for the table.
# Use the same group name as the map data.
brew_data <- as_tibble(breweries91) %>%
  select(brewery, address, village, founded) %>%
  SharedData$new(group = "breweries")

map <- leaflet(brew_sp) %>%
  addTiles() %>%
  addMarkers()

tbl <- reactable(
  brew_data,
  selection = "multiple",
  onClick = "select",
  rowStyle = list(cursor = "pointer"),
  minRows = 10
)

htmltools::tagList(map, tbl)