rguides

Interactive Tables with reactable

The reactable package builds interactive data tables for R using React. These tables run in R Markdown documents, Shiny applications, and standalone HTML pages. Reactable tables support sorting, filtering, pagination, row selection, and custom cell rendering without requiring JavaScript knowledge.

installation

Install reactable from CRAN:

install.packages("reactable")

You also need the htmlwidgets package for rendering:

install.packages("htmlwidgets")

For advanced formatting features, install the reactablefmtr package:

install.packages("reactablefmtr")

basic-usage

Create a basic interactive table with the reactable() function:

library(reactable)

# Sample data
sample_data <- data.frame(
  name = c("Alice", "Bob", "Charlie", "Diana", "Eve"),
  age = c(28, 35, 42, 31, 29),
  score = c(85, 92, 78, 88, 95),
  department = c("Engineering", "Marketing", "Sales", "Engineering", "HR")
)

# Create basic table
reactable(sample_data)

This renders a table with sortable columns, a search box, and pagination controls. Click any column header to sort. Use the search box to filter rows.

column-definitions-and-formatting

Control column behavior with colDef() within columns argument:

reactable(
  sample_data,
  columns = list(
    name = colDef(name = "Employee Name"),
    age = colDef(
      name = "Age",
      format = colFormat(suffix = " years")
    ),
    score = colDef(
      name = "Performance Score",
      format = colFormat(digits = 1),
      style = function(value) {
        color <- if (value >= 90) "#2ecc71" else if (value >= 80) "#f39c12" else "#e74c3c"
        list(color = color)
      }
    ),
    department = colDef(
      name = "Department",
      filterable = TRUE,
      cell = function(value) {
        paste0("📁 ", value)
      }
    )
  )
)

Column definitions accept:

  • name: Display name
  • width: Column width in pixels
  • align: Alignment (“left”, “center”, “right”)
  • format: Number/date formatting
  • sortable: Enable/disable sorting
  • filterable: Enable/disable filtering
  • resizable: Allow column resize
  • cell: Custom cell renderer

sorting-and-filtering

Enable sorting on specific columns:

reactable(
  sample_data,
  sortable = TRUE,
  defaultSorted = "score",
  defaultSortOrder = "desc",
  columns = list(
    name = colDef(sortable = TRUE),
    age = colDef(sortable = TRUE),
    score = colDef(sortable = TRUE),
    department = colDef(sortable = TRUE)
  )
)

Add column-specific filters:

reactable(
  sample_data,
  filterable = TRUE,
  columns = list(
    department = colDef(
      filterMethod = JS("function(rows, columnId, filterValue) {
        return rows.filter(function(row) {
          return row.values[columnId].toLowerCase().includes(filterValue.toLowerCase())
        })
      }")
    )
  )
)

The default filter performs case-insensitive substring matching. Use custom JavaScript functions for advanced filtering logic.

pagination

Control pagination with these options:

reactable(
  sample_data,
  pagination = TRUE,
  pageSize = 10,
  showPageSizeOptions = TRUE,
  pageSizeOptions = c(5, 10, 25, 50),
  defaultPage = 1
)
  • pagination: Enable pagination (default: TRUE for >10 rows)
  • pageSize: Default rows per page
  • showPageSizeOptions: Display page size selector
  • pageSizeOptions: Available page sizes
  • defaultPage: Initial page number

For client-side pagination, all data loads at once. For server-side pagination in Shiny, use remote() option and handle page events in your server function.

custom-cell-rendering

Render custom cell content with the cell parameter:

# Custom cell with progress bar
reactable(
  sample_data,
  columns = list(
    score = colDef(
      cell = function(value) {
        # Create progress bar
        width <- paste0(value, "%")
        bar <- htmltools::tags$div(
          style = list(width = width, background = "#3498db", height = "100%"),
          class = "progress-bar"
        )
        container <- htmltools::tags$div(
          style = list(width = "100%", background = "#ecf0f1", height = "20px"),
          bar
        )
        htmltools::tagList(container, paste(value, "points"))
      }
    )
  )
)

Use htmltools to build complex HTML elements within cells. This approach works for embedding:

  • Images and icons
  • Links and buttons
  • Progress bars and meters
  • Nested tables
  • Any HTML content

conditional-styling

Apply conditional formatting based on cell values:

reactable(
  sample_data,
  columns = list(
    score = colDef(
      style = function(value) {
        # Color based on score ranges
        color <- case_when(
          value >= 90 ~ "#27ae60",
          value >= 80 ~ "#2980b9",
          value >= 70 ~ "#f39c12",
          TRUE ~ "#c0392b"
        )
        list(color = color, fontWeight = "bold")
      }
    ),
    age = colDef(
      style = function(value) {
        # Highlight ages above 35
        if (value > 35) {
          list(background = "#ffe6e6")
        } else {
          list(background = "white")
        }
      }
    )
  )
)

Apply row-level styling with the rowStyle option:

reactable(
  sample_data,
  rowStyle = function(index) {
    if (sample_data$score[index] < 80) {
      list(background = "#fff5f5")
    }
  }
)

interactive-features

Reactable tables support user interaction beyond basic sorting and filtering:

Row selection

reactable(
  sample_data,
  selection = "multiple",
  onClick = "select",
  rowClass = function(index) {
    if (index %% 2 == 0) "even-row"
  }
)

Expandable rows

reactable(
  sample_data,
  details = function(index) {
    details_data <- sample_data[index, ]
    htmltools::tags$div(
      htmltools::tags$h4("Additional Details"),
      htmltools::tags$p(paste("Employee ID:", 1000 + index)),
      htmltools::tags$p(paste("Start Date:", Sys.Date() - sample_data$age[index] * 365))
    )
  }
)

Embed in Shiny

Use reactable in Shiny applications:

library(shiny)

ui <- fluidPage(
  titlePanel("Employee Dashboard"),
  reactableOutput("table")
)

server <- function(input, output) {
  output$table <- renderReactable({
    reactable(
      sample_data,
      columns = list(
        name = colDef(cell = function(value) {
          htmltools::tags$a(href = paste0("/employee/", value), value)
        })
      ),
      searchable = TRUE
    )
  })
}

shinyApp(ui, server)

see-also

Custom cell rendering

reactable’s colDef(cell = ...) argument accepts a JavaScript function as a string or an R function. In R, colDef(cell = function(value, index) htmltools::tags$strong(value)) wraps the cell content in bold. colDef(cell = JS("function(cellInfo) { return cellInfo.value > 100 ? '<b>' + cellInfo.value + '</b>' : cellInfo.value }")) uses JavaScript for client-side rendering without round-tripping to the server.

Aggregation

When a reactable has grouped rows, aggregate in colDef() specifies how grouped values are summarized: aggregate = "sum" sums numeric values, aggregate = "count" counts non-null values, aggregate = "mean" averages them. Custom aggregation with a JavaScript function: aggregate = JS("function(values) { return Math.max.apply(null, values) }"). Aggregation runs entirely in the browser, no server round-trip needed.

Integration with Shiny

reactable::reactableOutput() and renderReactable() integrate with Shiny. getReactableState("tableId", "selected") retrieves the currently selected rows as a reactive, enabling drill-down workflows. updateReactable("tableId", data = new_data) updates the table data without re-rendering the full UI. For large datasets, enable server-side pagination with reactable() wrapping a shiny::eventReactive that fetches only the requested page.

Theming

reactable integrates with bslib for Bootstrap-based theming. Set theme = reactableTheme(backgroundColor = "#f9f9f9", borderColor = "#ddd") for custom colors. The style argument on colDef() accepts CSS strings for per-column styles. For dark mode support, bslib::bs_theme() propagates Bootstrap variables to reactable automatically when used in a themed Shiny app.

Searching and filtering

reactable(df, searchable = TRUE) adds a global search box that filters all columns. filterable = TRUE adds per-column filter inputs. To filter specific columns only, set colDef(filterable = TRUE) for selected columns and leave others unset. For custom filter logic, colDef(filterMethod = JS("function(rows, columnId, filterValue) {...}")) defines a JavaScript filter function. Combine with defaultPageSize = 20 and pagination to handle tables with thousands of rows efficiently.

Interactive tables with reactable

reactable creates interactive HTML tables with sorting, filtering, and pagination. It renders as an htmlwidget in R Markdown, Quarto, and Shiny. The default output is clean and usable without customization.

reactable(df) renders a basic interactive table. searchable = TRUE adds a global search box. filterable = TRUE adds per-column filter inputs. sortable = TRUE (default) allows column sorting by clicking headers. pagination = FALSE shows all rows at once (careful with large data). defaultPageSize = 25 sets rows per page.

columns = list(col_name = colDef(...)) customizes individual columns. colDef(name = "Display Name", format = colFormat(digits = 2, percent = TRUE), minWidth = 80, align = "right") sets column display options. colDef(show = FALSE) hides a column from display while keeping it in the data for use in other column definitions.

Cell formatting and rendering

colDef(format = colFormat(prefix = "$", separators = TRUE)) formats cells as currency with comma separators. colDef(format = colFormat(datetime = TRUE, locales = "en-US")) formats datetime columns. colDef(html = TRUE) renders cell contents as HTML, enabling links, badges, and icons in cells.

colDef(cell = function(value, index, name) { ... }) provides full control over cell rendering. The function returns a string or htmltools tag. For conditional formatting: cell = function(value) { if (value > threshold) htmltools::tags$span(style = "color: red;", value) else value }.

colDef(style = function(value) { list(background = ifelse(value > 0, "lightgreen", "lightpink")) }) applies inline styles based on cell values. This achieves heatmap-style coloring without the complexity of custom rendering.

Column groups and spanning headers

columnGroups = list(colGroup(name = "Q1 Metrics", columns = c("jan", "feb", "mar"))) creates a spanning header over multiple columns. This mirrors Excel’s merged header cells and is useful for grouped data with multiple metrics per category.

colDef(aggregate = "sum") computes subtotals in grouped rows. Other aggregation options: "mean", "max", "min", "count", "unique", "frequency" (value counts), or a custom function.

Row groups and expandable rows

groupBy = "category" groups rows under expandable category headers. defaultExpanded = TRUE expands groups by default. Combined with aggregate, groups show summary values while rows show detail.

details = function(index) { htmltools::div(detailed_content) } creates expandable row details. Clicking a row expands it to show the returned content. This is the standard pattern for master-detail layouts — the main table shows summary data, expanding a row reveals the full record.

Theming and Shiny integration

reactableTheme(color = "#333", backgroundColor = "#fff", borderColor = "#ddd") sets table-wide colors. reactableTheme(headerStyle = list(background = "#f5f5f5")) styles just the headers. The glin/reactablefmtr package provides a high-level theming API with pre-built themes.

In Shiny: reactableOutput("table_id") in the UI; output$table_id <- renderReactable({ reactable(data) }) in the server. updateReactable("table_id", data = new_data) updates the table data without re-rendering the entire widget, preserving scroll position and expanded rows.

reactable::getReactableState("table_id", "selected") in Shiny returns the selected row indices. onClick = "select" enables row selection. This powers click-to-filter dashboards where selecting a table row updates other charts.

What makes reactable different

Most R table packages produce static output — you see the data as a formatted grid, but cannot interact with it. reactable produces HTML tables with built-in client-side sorting, filtering, and pagination powered by the React Table library. The entire table renders in the browser and responds to user interaction without any server involvement. This makes reactable tables appropriate for HTML reports, Shiny apps, and static websites that need exploratory data interaction.

The package builds tables declaratively. You describe the desired column behavior and appearance, and reactable handles the implementation. Columns can have custom renderers that produce arbitrary HTML — sparklines, progress bars, colored badges, or formatted numbers. This flexibility comes at the cost of complexity: a production reactable table with rich formatting requires more code than an equivalent gt or kableExtra table, but the result is interactive in ways those packages cannot match.

Grouping and aggregation

reactable supports groupBy columns that collapse rows into groups with expandable detail rows. The aggregate argument on a column definition specifies how values in grouped rows should be summarized — sum, mean, max, min, count, or a custom JavaScript function. Groups can be nested: grouping by region then by department creates a two-level expandable hierarchy.

When designing grouped tables, choose summary functions that are meaningful for each column type. Summing a rate column produces a nonsensical result; averaging it may also be wrong if group sizes differ. Numeric columns that represent counts should use sum; columns that represent rates or proportions should use a weighted mean or be left blank in grouped rows to avoid misleading summaries.

Pagination and performance

By default, reactable paginates large tables showing ten rows per page. The defaultPageSize argument controls this. For small datasets where showing everything at once is better than pagination, set defaultPageSize to the number of rows or use pagination = FALSE. Searching and sorting still work with pagination disabled.

For very large tables, client-side reactable tables can be slow because all data is embedded in the HTML. Tables with more than a few thousand rows benefit from server-side filtering and pagination, which requires Shiny for the data management piece. The reactable package integrates with Shiny through reactable’s server-side API, where R code handles filtering and pagination and sends only the current page’s data to the browser.