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 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.
Note: Ascending order means the lowest, first, or earliest values will appear first. Descending order means the largest, last, or latest values will appear first.
Default sorted columns
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:
Default sort 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.
Sort missing values last
You can ignore missing values when sorting by setting
sortNALast
on a column:
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
:
Hide sort icons
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)
Filtering
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
)
Custom filtering
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
)
Searching
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)
Custom searching
The table search method can be customized using the
searchMethod
argument in reactable()
. See the
Custom Filtering guide for details
and examples.
Pagination
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)
Page size options
You can show a dropdown of page sizes for users to choose from using
showPageSizeOptions
. The page size options can be
customized through pageSizeOptions
:
Alternative pagination types
You can use an alternative pagination type by setting
paginationType
to:
-
"jump"
to show a page jump -
"simple"
to show previous/next buttons only
Page jump
reactable(iris[1:50, ], paginationType = "jump", defaultPageSize = 4)
Simple
reactable(iris[1:50, ], paginationType = "simple", defaultPageSize = 4)
Hide page info
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)
Always show pagination
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)
No pagination
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)
Tip: 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.
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 values 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:
= list(
columns = colDef(
Price = JS("function(values, rows) {
aggregate
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")
)
)
)
Include sub rows in pagination
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.
Column Formatting
You can format data in a column by providing colFormat()
options to the format
argument in
colDef()
.
The formatters for numbers, dates, times, and currencies are locale-sensitive and automatically adapt to language preferences of the user’s browser. This means, for example, that users will see dates formatted in their own timezone or numbers formatted in their own locale.
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.
Note: Column formatters change how data is displayed without affecting the underlying data. Sorting, filtering, and grouping will still work on the original data.
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"))
))
Date formatting
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"))
))
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:
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 HTML widgets, 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.
Note: Custom rendering changes how data is displayed without affecting the underlying data. Sorting, filtering, and grouping will still work on the original data.
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 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 = ","))
})
))
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
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()
}
"))
))
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]])
})
))
Grouped cell rendering
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
}")
)
)
)
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) {
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)
}")
)
)
)
Header rendering
This example requires reactable v0.3.0 or above.
library(htmltools)
reactable(
iris[1:5, ],
defaultColDef = colDef(header = function(value) {
units <- div(style = "color: #737373", "cm")
div(title = value, value, units)
}),
columns = list(
Petal.Width = colDef(
name = "Petal Width",
html = TRUE,
align = "left",
header = JS('function(column) {
return column.name + `<div style="color: #737373">cm</div>`
}')
),
Species = colDef(header = function(value) {
tags$a(href = "https://wikipedia.org/wiki/List_of_Iris_species", value)
})
)
)
Custom metadata
New in v0.4.0
You can pass arbitrary data from R to JavaScript render functions
using the meta
argument in reactable()
.
meta
should be a named list of values that can also be
JS()
expressions or functions. Custom metadata can be
accessed from JavaScript using the state.meta
property, and
updated using updateReactable()
in Shiny or Reactable.setMeta()
in the JavaScript API.
Use custom metadata to:
- Simplify JavaScript render functions that need access to data outside of the table
- Dynamically change how data is formatted without rerendering the table
- Share JavaScript code or data between different render functions
library(htmltools)
data <- MASS::Cars93[1:6, c("Manufacturer", "Model", "Type", "Price", "MPG.city")]
exchange_rates <- list(
USD = 1,
CAD = 1.30,
JPY = 137.56
)
tbl <- reactable(
data,
columns = list(
Price = colDef(
cell = JS("function(cellInfo, state) {
const { currency, exchangeRates } = state.meta
const converted = cellInfo.value * exchangeRates[currency]
return converted.toLocaleString(undefined, { style: 'currency', currency: currency })
}")
)
),
meta = list(
currency = "USD",
exchangeRates = exchange_rates
),
elementId = "cars-currency-table"
)
browsable(
tagList(
tags$label(
"Currency",
tags$select(
onchange = "Reactable.setMeta('cars-currency-table', { currency: event.target.value })",
lapply(names(exchange_rates), tags$option)
)
),
tags$hr("aria-hidden" = "true"),
tbl
)
)
Footers
You can add column footers using the footer
argument in
colDef()
.
footer
can either be custom content to render (e.g., a
character string or HTML tag), or a custom render function. See Custom Rendering to learn more about
using custom 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
This example requires reactable v0.3.0 or above.
reactable(
data,
searchable = TRUE,
defaultPageSize = 5,
minRows = 5,
columns = list(
Manufacturer = colDef(footer = "Total"),
Sales = colDef(
footer = JS("function(column, state) {
let total = 0
state.sortedData.forEach(function(row) {
total += row[column.id]
})
return '$' + total.toFixed(2)
}")
)
),
defaultColDef = colDef(footerStyle = list(fontWeight = "bold"))
)
Expandable Row Details
This example requires reactable v0.3.0 or above.
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.values, null, 2)}</pre>`
}"),
html = TRUE,
width = 60
))
Nested tables
With R render functions, you can render HTML tags, HTML widgets, and even nested tables:
Conditional row details
R render functions support conditional rendering. If a render
function returns NULL
, the row won’t be expandable:
Multiple row details
This example requires reactable v0.3.0 or above.
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.values['Sepal.Length']
}
"))
)
)
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
Row styling
R style function
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"
}
}
)
.bold {
font-weight: bold;
}
Custom metadata
New in v0.4.0
You can pass arbitrary data from R to JavaScript style functions
using the meta
argument in reactable()
.
meta
should be a named list of values that can also be
JS()
expressions or functions. Custom metadata can be
accessed from JavaScript using the state.meta
property, and
updated using updateReactable()
in Shiny or Reactable.setMeta()
in the JavaScript API.
Use custom metadata to:
- Simplify JavaScript style functions that need access to data outside of the table
- Dynamically change how data is styled without rerendering the table
- Share JavaScript code or data between different style functions
library(htmltools)
data <- MASS::Cars93[1:6, c("Manufacturer", "Model", "Type", "Price", "MPG.city")]
mpg_normalized <- (data$MPG.city - min(data$MPG.city)) / (max(data$MPG.city) - min(data$MPG.city))
mpg_colors <- rgb(colorRamp(c("#ffe4cc", "#ff9f1a"))(mpg_normalized), maxColorValue = 255)
tbl <- reactable(
data,
columns = list(
MPG.city = colDef(
style = JS("function(rowInfo, column, state) {
const { showColors, mpgColors } = state.meta
if (showColors) {
return { backgroundColor: mpgColors[rowInfo.index] }
}
}")
)
),
meta = list(
mpgColors = mpg_colors,
showColors = TRUE
),
elementId = "cars-colors-table"
)
browsable(
tagList(
tags$label(
tags$input(
type = "checkbox",
checked = NA,
onclick = "Reactable.setMeta('cars-colors-table', function(prevMeta) {
return { showColors: !prevMeta.showColors }
})"
),
"Show color scale"
),
tags$hr("aria-hidden" = "true"),
tbl
)
)
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
:
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.
Scrollable tables are automatically made focusable when navigating using a keyboard to ensure that they’re always accessible for keyboard users.
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 to100
) -
maxWidth
- maximum width of the column in pixels -
width
- fixed width of the column in pixels (overridesminWidth
andmaxWidth
)
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:
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
)
Vertical alignment
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 = "0.75rem"), species)
)
}
),
species = colDef(show = FALSE)
),
# Vertically center cells and bottom-align headers
defaultColDef = colDef(vAlign = "center", headerVAlign = "bottom"),
bordered = TRUE
)
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 sometimes better to add CSS through an external style sheet. To learn more about adding custom CSS through an external style sheet:
Note: If you inspect a table’s HTML, you might find
CSS classes like .rt-table
on different elements of the
table. These CSS classes are undocumented and subject to change, so we
recommend adding your own custom class names, or using themes to customize parts of the table that aren’t
covered by the custom class names.
Theming
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"))
)
)
Sticky Columns
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)
)
Multiple sticky columns
# 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
)
Sticky column groups
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 and Row Headers
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)
Row headers
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.