🎯 Goal

In this code‑through, we’ll learn how to create interactive, sortable, searchable tables using the {reactable} package. We’ll start simple and layer on features commonly needed in evaluation reports and dashboards: formatting, conditional highlighting, mini in‑cell visuals, filters, and themes.

Why reactable? Unlike static tables, reactable lets your audience explore data directly in your knitted HTML without needing Shiny.

📦 Packages

library(reactable)
library(reactablefmtr) # optional helpers like data_bars(), color_scales()
library(tidyverse)
library(scales)
library(tibble)

📄 Data: mtcars (swap with your own)

We’ll use mtcars for a reproducible demo and make row names a column for readability. You can replace this with any dataframe (e.g., program metrics, survey items, player stats).

data <- mtcars %>%
  rownames_to_column("model") %>%
  mutate(
    cyl = factor(cyl),
    am  = factor(am, labels = c("Automatic", "Manual")),
    # create a couple of demo variables for formatting
    mpg_rank = min_rank(desc(mpg)),
    hp_pct   = rescale(hp, to = c(0, 1))
  )

dplyr::glimpse(data)
## Rows: 32
## Columns: 14
## $ model    <chr> "Mazda RX4", "Mazda RX4 Wag", "Datsun 710", "Hornet 4 Drive",…
## $ mpg      <dbl> 21.0, 21.0, 22.8, 21.4, 18.7, 18.1, 14.3, 24.4, 22.8, 19.2, 1…
## $ cyl      <fct> 6, 6, 4, 6, 8, 6, 8, 4, 4, 6, 6, 8, 8, 8, 8, 8, 8, 4, 4, 4, 4…
## $ disp     <dbl> 160.0, 160.0, 108.0, 258.0, 360.0, 225.0, 360.0, 146.7, 140.8…
## $ hp       <dbl> 110, 110, 93, 110, 175, 105, 245, 62, 95, 123, 123, 180, 180,…
## $ drat     <dbl> 3.90, 3.90, 3.85, 3.08, 3.15, 2.76, 3.21, 3.69, 3.92, 3.92, 3…
## $ wt       <dbl> 2.620, 2.875, 2.320, 3.215, 3.440, 3.460, 3.570, 3.190, 3.150…
## $ qsec     <dbl> 16.46, 17.02, 18.61, 19.44, 17.02, 20.22, 15.84, 20.00, 22.90…
## $ vs       <dbl> 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1…
## $ am       <fct> Manual, Manual, Manual, Automatic, Automatic, Automatic, Auto…
## $ gear     <dbl> 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 4, 3…
## $ carb     <dbl> 4, 4, 1, 1, 2, 1, 4, 2, 2, 4, 4, 3, 3, 3, 4, 4, 4, 1, 2, 1, 1…
## $ mpg_rank <int> 13, 13, 8, 11, 18, 19, 29, 7, 8, 16, 20, 22, 21, 25, 31, 31, …
## $ hp_pct   <dbl> 0.20494700, 0.20494700, 0.14487633, 0.20494700, 0.43462898, 0…

1) The Smallest Working Example

reactable_dl(data)
  • Default table is already sortable and scrollable.

2) Rename & Format Columns with colDef()

  • Use colDef(name=...) to rename headers
  • Use colFormat() for numeric formatting (digits, percent, currency)
reactable_dl(
  data,
  columns = list(
    model = colDef(name = "Model", sticky = "left", width = 160),
    mpg   = colDef(name = "Miles/Gallon", format = colFormat(digits = 1)),
    cyl   = colDef(name = "Cylinders", align = "center", width = 100),
    disp  = colDef(name = "Displacement (cu in)", format = colFormat(digits = 0)),
    hp    = colDef(name = "Horsepower", align = "center"),
    hp_pct = colDef(name = "HP (0–1)", format = colFormat(percent = TRUE, digits = 0)),
    wt    = colDef(name = "Weight (1000 lbs)", format = colFormat(digits = 2)),
    am    = colDef(name = "Transmission")
  )
)

3) Search, Pagination, & Column Filters

reactable_dl(
  data,
  searchable = TRUE,
  pagination = TRUE,
  defaultPageSize = 10,
  highlight = TRUE,
  filterable = TRUE
)
  • searchable = TRUE adds a global search box
  • filterable = TRUE adds per‑column filters

4) Conditional Formatting with Color Scales

Two quick styles: continuous color scale and in‑cell data bars. These helpers come from {reactablefmtr}.

reactable_dl(
  data,
  columns = list(
    mpg = colDef(
      name = "Miles/Gallon",
      cell = function(value) reactablefmtr::color_tiles(value, colors = c("#fde0dd", "#fa9fb5", "#c51b8a")),
      align = "center"
    ),
    hp = colDef(
      name = "Horsepower",
      cell = function(value) reactablefmtr::data_bars(value,
                       text_position = "inside-end",
                       number_fmt = scales::label_number(accuracy = 0.1)),
      align = "center"
    ),
    wt = colDef(
      name = "Weight",
      cell = function(value) reactablefmtr::color_scales(value, colors = c("#f0f9e8", "#7bccc4", "#084081")),
      align = "center"
    ),
    model = colDef(name = "Model", sticky = "left", width = 160)
  ),
  defaultPageSize = 12,
  bordered = TRUE,
  striped = TRUE
)

5) Grouping, Column Groups, & Sorting Defaults

You can group rows by a factor and add column groups to organize headers.

reactable_dl(
  data %>% arrange(cyl, desc(mpg)),
  groupBy = "cyl",
  defaultSorted = list(mpg = "desc"),
  columns = list(
    model = colDef(name = "Model", sticky = "left"),
    cyl   = colDef(name = "Cyl"),
    mpg   = colDef(name = "MPG", format = colFormat(digits = 1)),
    hp    = colDef(name = "HP"),
    disp  = colDef(name = "Disp"),
    wt    = colDef(name = "Weight", format = colFormat(digits = 2)),
    am    = colDef(name = "Trans")
  ),
  columnGroups = list(
    colGroup(name = "Performance", columns = c("mpg", "hp", "disp")),
    colGroup(name = "Specs",       columns = c("wt", "am"))
  ),
  striped = TRUE,
  bordered = TRUE
)

6) Add Row Details (Expandable Rows)

Great for metadata, notes, or longer text fields.

reactable_dl(
  data,
  details = function(index) {
    row <- data[index, ]
    htmltools::div(style = "padding: 8px;",
      htmltools::tags$strong(row$model), htmltools::br(),
      sprintf("MPG Rank: %s", row$mpg_rank), htmltools::br(),
      sprintf("Transmission: %s", row$am)
    )
  },
  defaultExpanded = FALSE,
  highlight = TRUE
)

7) Download Button & CSV Export

Enable a one‑click CSV export.

reactable_dl(
  data,
  searchable = TRUE,
  filterable = TRUE,
  pagination = TRUE,
  defaultPageSize = 10,
  downloadButton = TRUE
)

8) Theming with reactableTheme()

Polish the look & feel to match a report or dashboard aesthetic.

reactable_dl(
  data,
  theme = reactableTheme(
    color = "#222",
    backgroundColor = "#fff",
    borderColor = "#e5e7eb",
    stripedColor = "#f9fafb",
    highlightColor = "#f1f5f9",
    searchInputStyle = list(borderColor = "#e5e7eb", background = "#fff"),
    rowSelectedStyle = list(backgroundColor = "#eef2ff"),
    headerStyle = list(background = "#f3f4f6", borderColor = "#e5e7eb",
                       fontWeight = 700, letterSpacing = "0.3px")
  ),
  striped = TRUE,
  bordered = TRUE,
  defaultPageSize = 12,
  highlight = TRUE,
  filterable = TRUE
)

9) Bring It All Together (Reusable Helper)

Create a small wrapper to standardize your table style across projects.

make_table <- function(df, page_size = 12) {
  reactable_dl(
    df,
    searchable = TRUE,
    filterable = TRUE,
    defaultPageSize = page_size,
    pagination = TRUE,
    highlight = TRUE,
    bordered = TRUE,
    striped = TRUE,
    downloadButton = TRUE,
    theme = reactableTheme(
      color = "#222", backgroundColor = "#fff", borderColor = "#e5e7eb",
      stripedColor = "#f9fafb", highlightColor = "#f1f5f9",
      headerStyle = list(background = "#f3f4f6", borderColor = "#e5e7eb",
                         fontWeight = 700)
    )
  )
}

make_table(data)

🔁 Swap in Your Own Data (2‑minute recipe)

  1. Put a CSV in your project folder, e.g., my_data.csv
  2. Read it, lightly clean, and pass to make_table()
# my <- readr::read_csv("my_data.csv") %>% janitor::clean_names()
# make_table(my)

🧭 Reflection & Use Cases

  • Program evaluation: interactive KPI tables with filters and quick CSV export
  • Class reports: let peers sort/search findings without extra tooling
  • Dashboards: drop into flexdashboard or quarto for instant interactivity

What I learned: reactable adds a lot of value with very little code. For many reports, it’s the fastest path to interactivity without building a Shiny app.