To create a data table, use reactable()
on a data frame or matrix. The table will be sortable and paginated by default:
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
)
Tables are sortable by default. You can sort a column by clicking on its header, or sort multiple columns by holding the shift key while sorting.
Sorting toggles between ascending and descending order by default. To clear the sort, hold the shift key while sorting, and the sorting will additionally toggle between ascending, descending, and unsorted order.
Ascending order means the lowest, first, or earliest values will appear first. Descending order means the largest, last, or latest values will appear first.
You can set the default sorted columns by providing a vector of column names to defaultSorted
:
You can also provide a named list to customize the default sort orders. Use "asc"
for ascending order, or "desc"
for descending order:
Columns are sorted in ascending order first by default. To change the default sort order for all columns in the table, set defaultSortOrder
in reactable()
to "asc"
for ascending order, or "desc"
for descending order.
To change the sort order of an individual column, set defaultSortOrder
in its colDef()
to "asc"
or "desc"
. The default sort order of the column takes precedence over the table.
You can ignore missing values when sorting by setting sortNALast
on a column:
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
:
You can hide sort icons by setting showSortIcon
to FALSE
. This is only recommended when you want to use a custom sort indicator.
reactable(iris[1:5, ], showSortIcon = FALSE)
You can make columns filterable by setting filterable = TRUE
in reactable()
:
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
to TRUE
or FALSE
in colDef()
:
reactable(
data,
filterable = TRUE,
columns = list(
Price = colDef(filterable = FALSE)
),
defaultPageSize = 5
)
Column filtering can be customized using the filterMethod
and filterInput
arguments in colDef()
. See the Custom Filtering guide for more details and examples.
This example shows basic usage of a custom filter method, changing filtering on the Manufacturer
column to be case-sensitive rather than case-insensitive. (Try filtering for “bmw” and then “BMW”).
data <- MASS::Cars93[, c("Manufacturer", "Model", "Type", "Price")]
reactable(
data,
columns = list(
Manufacturer = colDef(
filterable = TRUE,
# Filter by case-sensitive text match
filterMethod = JS("function(rows, columnId, filterValue) {
return rows.filter(function(row) {
return row.values[columnId].indexOf(filterValue) !== -1
})
}")
)
),
defaultPageSize = 5
)
You can make the entire table searchable by setting searchable = TRUE
in reactable()
:
data <- MASS::Cars93[1:20, c("Manufacturer", "Model", "Type", "AirBags", "Price")]
reactable(data, searchable = TRUE, minRows = 10)
The table search method can be customized using the searchMethod
argument in reactable()
. See the Custom Filtering guide for details and examples.
You can change the default page size by configuring defaultPageSize
:
reactable(iris[1:6, ], defaultPageSize = 4)
You can also set the minimum rows per page using minRows
. This may be useful when rows don’t completely fill the page, or if the table has filtering:
reactable(iris[1:6, ], defaultPageSize = 4, minRows = 4, searchable = TRUE)
You can show a dropdown of page sizes for users to choose from using showPageSizeOptions
. The page size options can be customized through pageSizeOptions
:
You can use an alternative pagination type by setting paginationType
to:
"jump"
to show a page jump"simple"
to show previous/next buttons only
reactable(iris[1:50, ], paginationType = "jump", defaultPageSize = 4)
reactable(iris[1:50, ], paginationType = "simple", defaultPageSize = 4)
You can hide page info by setting showPageInfo
to FALSE
:
reactable(iris[1:12, ], showPageInfo = FALSE, defaultPageSize = 4)
reactable(iris[1:12, ], showPageInfo = FALSE, showPageSizeOptions = TRUE, defaultPageSize = 4)
By default, pagination is hidden if the table only has one page. To keep the pagination shown, set showPagination
to TRUE
. This is especially useful if you want to keep the page info showing the number of rows in the table.
reactable(iris[1:5, ], showPagination = TRUE)
Tables are paginated by default, but you can disable pagination by setting pagination
to FALSE
:
reactable(iris[1:20, ], pagination = FALSE, highlight = TRUE, height = 250)
NOTE: Disabling pagination is not recommended for large tables with many interactive elements (such as links, expand buttons, or selection checkboxes), as that can make it difficult for keyboard users to navigate the page.
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 values for all rows in the group (optional)
//
// output:
// - an aggregated value, e.g. a comma-separated list
return values.join(', ')
}
")
)
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 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:
columns = list(
Price = colDef(
aggregate = JS("function(values, rows) {
values
// [46.8, 27.6, 57]
rows
// [
// { "Model": "Dynasty", "Manufacturer": "Dodge", "Price": 46.8, "Units": 2 },
// { "Model": "Colt", "Manufacturer": "Dodge", "Price": 27.6, "Units": 5 },
// { "Model": "Caravan", "Manufacturer": "Dodge", "Price": 57, "Units": 5 }
// ]
}")
)
)
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) {
let totalPrice = 0
let totalUnits = 0
rows.forEach(function(row) {
totalPrice += row['Price']
totalUnits += row['Units']
})
return totalPrice / totalUnits
}"),
format = colFormat(currency = "USD")
)
)
)
New in v0.3.0
By default, sub rows are excluded from pagination and always shown on the same page when expanded. To include sub rows in pagination, you can set paginateSubRows
to TRUE
. This is recommended for grouped tables with a large number of rows where expanded rows may not all fit on one page.
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 a list of common BCP 47 language tags for reference.
datetimes <- as.POSIXct(c("2019-01-02 3:22:15", "2019-03-15 09:15:55", "2019-09-22 14:20:00"),
tz = "America/New_York")
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"))
))
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")
)
))
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)
)
)
)
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:
If none of the built-in formatters apply to your data, you can use a custom cell renderer instead.
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.
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 an X mark or check mark
if (value == "None") "\u274c No" else "\u2714\ufe0f Yes"
}),
Price = colDef(cell = function(value) {
# Render as currency
paste0("$", format(value * 1000, big.mark = ","))
})
))
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
const 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 an X mark or check mark
return cellInfo.value === 'None' ? '\u274c No' : '\u2714\ufe0f Yes'
}
")),
Price = colDef(cell = JS("
function(cellInfo) {
// Render as currency
return '$' + (cellInfo.value * 1000).toLocaleString()
}
"))
))
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]])
})
))
New in v0.3.0
data <- MASS::Cars93[10:22, c("Manufacturer", "Model", "Type", "Price", "MPG.city")]
reactable(
data,
groupBy = c("Manufacturer", "Type"),
columns = list(
Manufacturer = colDef(
# Render grouped cells without the row count
grouped = JS("function(cellInfo) {
return cellInfo.value
}")
),
Type = colDef(
# Render grouped cells with the row count, only if there are multiple sub rows
grouped = JS("function(cellInfo) {
if (cellInfo.subRows.length > 1) {
return cellInfo.value + ' (' + cellInfo.subRows.length + ')'
}
return cellInfo.value
}")
)
)
)
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) {
const values = cellInfo.subRows.map(function(row) { return row['Type'] })
const 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)
}")
)
)
)
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)
})
)
)
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.
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"))
)
reactable(
data,
searchable = TRUE,
defaultPageSize = 5,
minRows = 5,
columns = list(
Manufacturer = colDef(footer = "Total"),
Sales = colDef(
footer = JS("function(colInfo) {
let total = 0
colInfo.data.forEach(function(row) {
total += row[colInfo.column.id]
})
return '$' + total.toFixed(2)
}")
)
),
defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)
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.row, null, 2) + '</pre>'
}"),
html = TRUE,
width = 60
))
With R render functions, you can render HTML tags, HTML widgets, and even nested tables:
R render functions support conditional rendering. If a render function returns NULL
, the row won’t be expandable:
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']
}
"))
)
)
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.
You can customize table styling using several options, which can all be combined:
reactable(iris[1:5, ], highlight = TRUE)
reactable(iris[1:5, ], bordered = TRUE)
reactable(iris[1:5, ], borderless = TRUE)
reactable(iris[1:5, ], outlined = TRUE)
reactable(iris[1:5, ], striped = TRUE)
reactable(iris[1:5, ], bordered = TRUE, striped = TRUE, highlight = TRUE)
reactable(iris[1:5, ], outlined = TRUE, borderless = TRUE)
reactable(iris[1:5, ], compact = TRUE)
Long text is wrapped by default, but you can force text to fit on a single line by setting wrap
to FALSE
:
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:
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 pixelswidth
- 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:
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:
New in v0.3.0
You can change the vertical alignment of cell content using the vAlign
or headerVAlign
arguments in colDef()
and colGroup()
. vAlign
controls the alignment of data cells, while headerVAlign
controls the alignment of header cells. Possible options are "top"
(the default), "center"
, and "bottom"
.
library(dplyr)
library(htmltools)
data <- starwars[1:6, ] %>%
select(character = name, height, mass, gender, homeworld, species)
reactable(
data,
columns = list(
character = colDef(
name = "Character / Species",
# 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)
),
# Vertically center cells and bottom-align headers
defaultColDef = colDef(vAlign = "center", headerVAlign = "bottom"),
bordered = TRUE,
width = 600
)
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 sometimes better to add CSS through an external style sheet. To learn more about adding custom CSS through an external style sheet:
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%")
)
)
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)
)
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)
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)
}
}
})
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"))
)
)
New in v0.3.0
You can make columns sticky when scrolling horizontally using the sticky
argument in colDef()
or colGroup()
. Set sticky
to either "left"
or "right"
to make the column stick to the left or right side.
reactable(
MASS::Cars93[1:5, ],
columns = list(
Manufacturer = colDef(
sticky = "left",
# Add a right border style to visually distinguish the sticky column
style = list(borderRight = "1px solid #eee"),
headerStyle = list(borderRight = "1px solid #eee")
),
Make = colDef(
sticky = "right",
# Add a left border style to visually distinguish the sticky column
style = list(borderLeft = "1px solid #eee"),
headerStyle = list(borderLeft = "1px solid #eee")
)
),
defaultColDef = colDef(minWidth = 150)
)
NOTE: Sticky columns are not supported in IE11.
Before v0.3.0, columns could be made sticky by applying custom styles. This method was harder to use and did not work well with column resizing, row highlighting, or row striping. See Sticky Columns for examples of the previous method.
# Background style to visually distinguish sticky columns
sticky_style <- list(backgroundColor = "#f7f7f7")
reactable(
MASS::Cars93[1:5, ],
columns = list(
Manufacturer = colDef(
sticky = "left",
style = sticky_style,
headerStyle = sticky_style
),
Model = colDef(
sticky = "left",
style = sticky_style,
headerStyle = sticky_style
),
Type = colDef(
sticky = "left",
style = sticky_style,
headerStyle = sticky_style
)
),
resizable = TRUE,
wrap = FALSE,
bordered = TRUE
)
If a column group is sticky, all columns in the group will automatically be made sticky.
reactable(
MASS::Cars93[1:5, ],
columnGroups = list(
colGroup("Make", columns = c("Manufacturer", "Model"), sticky = "left"),
colGroup("Price", columns = c("Min.Price", "Price", "Max.Price"), sticky = "left")
),
defaultColDef = colDef(footer = "Footer"),
resizable = TRUE,
wrap = FALSE,
bordered = TRUE
)
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)
New in v0.3.0
You can mark up cells in a column as row headers by setting rowHeader
to TRUE
in colDef()
.
Use this to help users navigate the table using assistive technologies. When cells are marked up as row headers, assistive technologies will read them aloud while navigating through cells in the table.
Cells in the row names column are automatically marked up as row headers.
You can add cell click actions using the onClick
argument, which accepts the following values:
"expand"
to expand the row"select"
to select the row
reactable(iris[1:5, ], selection = "multiple", onClick = "select")
This example uses a custom click action to create custom “show details” action buttons in each row of the table:
data <- cbind(
MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "Price")],
details = NA
)
reactable(
data,
columns = list(
# Render a "show details" button in the last column of the table.
# This button won't do anything by itself, but will trigger the custom
# click action on the column.
details = colDef(
name = "",
sortable = FALSE,
cell = function() htmltools::tags$button("Show details")
)
),
onClick = JS("function(rowInfo, column) {
// Only handle click events on the 'details' column
if (column.id !== 'details') {
return
}
// Display an alert dialog with details for the row
window.alert('Details for row ' + rowInfo.index + ':\\n' + JSON.stringify(rowInfo.row, null, 2))
// Send the click event to Shiny, which will be available in input$show_details
// Note that the row index starts at 0 in JavaScript, so we add 1
if (window.Shiny) {
Shiny.setInputValue('show_details', { index: rowInfo.index + 1 }, { priority: 'event' })
}
}")
)
WARNING: Custom click actions are currently not accessible to keyboard users, and are generally not recommended. If they must be used, ensure that they can be triggered by a keyboard through other means, such as a button in the example above.
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"
)
)
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)
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)
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.
You can preselect rows by specifying a vector of row indices in defaultSelected
:
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' }
}
}")
)
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)
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)
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 Using Crosstalk in the Crosstalk package documentation.
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)
)
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::browsable(
htmltools::tagList(map, tbl)
)
New in v0.3.0
You can use the JavaScript API to create custom interactive controls for your table without the use of Shiny, or add cross-widget interactions beyond what Crosstalk provides.
See the JavaScript API guide for details on how to use the JavaScript API in R Markdown documents or Shiny apps, and the full API reference.
library(htmltools)
library(fontawesome)
data <- MASS::Cars93[1:15, c("Manufacturer", "Model", "Type", "Price")]
htmltools::browsable(
tagList(
tags$button(
tagList(fontawesome::fa("download"), "Download as CSV"),
onclick = "Reactable.downloadDataCSV('cars-download-table', 'cars.csv')"
),
reactable(
data,
searchable = TRUE,
defaultPageSize = 5,
elementId = "cars-download-table"
)
)
)
library(htmltools)
data <- MASS::Cars93[1:15, c("Manufacturer", "Model", "Type", "Price")]
htmltools::browsable(
tagList(
div(
div(tags$label("Filter Type", `for` = "cars-type-filter")),
tags$select(
id = "cars-type-filter",
onchange = "Reactable.setFilter('cars-filter-table', 'Type', this.value)",
tags$option("All", value = ""),
lapply(unique(data$Type), tags$option)
)
),
tags$hr("aria-hidden" = "false"),
reactable(data, defaultPageSize = 5, elementId = "cars-filter-table")
)
)
library(htmltools)
data <- MASS::Cars93[1:15, c("Manufacturer", "Model", "Type", "Price")]
htmltools::browsable(
tagList(
div(
style = "margin-bottom: 12px",
tags$input(
type = "text",
placeholder = "Search for cars...",
style = "padding: 4px 8px; width: 100%",
oninput = "Reactable.setSearch('cars-search-table', this.value)"
)
),
reactable(data, defaultPageSize = 5, elementId = "cars-search-table")
)
)
library(dplyr)
library(htmltools)
set.seed(10)
data <- sample_n(tail(MASS::Cars93, 9), 30, replace = TRUE) %>%
select(Manufacturer, Model, Type, Sales = Price)
htmltools::browsable(
tagList(
div(tags$label("Group by", `for` = "cars-grouping-select")),
tags$select(
id = "cars-grouping-select",
onchange = "Reactable.setGroupBy('cars-grouping-table', this.value ? [this.value] : [])",
tags$option("None", value = ""),
lapply(c("Manufacturer", "Model", "Type"), tags$option)
),
tags$hr("aria-hidden" = "false"),
reactable(
data,
columns = list(
Manufacturer = colDef(aggregate = "unique"),
Model = colDef(aggregate = "unique"),
Type = colDef(aggregate = "unique"),
Sales = colDef(aggregate = "sum", format = colFormat(currency = "USD"))
),
defaultPageSize = 5,
minRows = 5,
elementId = "cars-grouping-table"
)
)
)
library(htmltools)
data <- MASS::Cars93[1:5, c("Manufacturer", "Model", "Type", "Price")]
htmltools::browsable(
tagList(
tags$button(
"Expand/collapse all",
onclick = "Reactable.toggleAllRowsExpanded('cars-expansion-table')"
),
reactable(
data,
groupBy = "Manufacturer",
defaultPageSize = 5,
elementId = "cars-expansion-table"
)
)
)