Creating interactive tables with reactable

Today I’m showing you how to create a nice interactive tables with the reactable package
Author

Albert Rapp

Published

August 18, 2024

The R programming language has a rich ecosystem of packages that are fantastic for creating beautiful production-grade tables from within R. Today, I’m showing you that one package that makes it really easy (mostly) to create interactive tables. Namely, I’m going to show you {reactable}. 🥳

If you want to see a video version of this blog post, you can find it on YouTube:

Fake data

Let’s first create a dummy data set using one of {gt}’s built-in data sets. {gt} has a lot of those so we might as well use it even if we don’t use {gt} to create the table in the end.

library(tidyverse)
library(reactable)
hawaiian_sales <- gt::pizzaplace |> 
  filter(name == 'hawaiian') |> 
  mutate(
    month = month(
      date, label = TRUE, abbr = FALSE,
      locale = 'en_US.UTF-8' # English month names
    ),
    quarter = paste0('Q', quarter(date))
  ) |> 
  summarise(
    sales = n(),
    revenue = sum(price),
    .by = c(month, quarter)
  )
hawaiian_sales
## # A tibble: 12 × 4
##    month     quarter sales revenue
##    <ord>     <chr>   <int>   <dbl>
##  1 January   Q1        185   2443.
##  2 February  Q1        198   2633 
##  3 March     Q1        217   2878.
##  4 April     Q2        219   2868.
##  5 May       Q2        198   2688 
##  6 June      Q2        189   2564.
##  7 July      Q3        195   2620.
##  8 August    Q3        201   2679.
##  9 September Q3        196   2616.
## 10 October   Q4        188   2515.
## 11 November  Q4        227   2953.
## 12 December  Q4        209   2817.

Base Layer

To create a table all we have to to is to pass the data to the reactable function.

reactable(hawaiian_sales)

The nice thing is that this is interactive out of the box. By clicking onto the column names, you can sort the rows.

Use better column names

Unlike {gt}, the {reactable} package doesn’t allow to change the table step by step by chaining pipes. Instead, you will have to use one of the many arguments of reactable() and helper functions to get things done. For example, to set nicer column names you can use the columns argument with a list of column definitions (using the colDef() helper)

reactable(
  hawaiian_sales,
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(name = 'Month'),
    sales = colDef(name = 'Sales'),
    revenue = colDef(name = 'Revenue')
  )
)

Title & Subtitle

For adding a nice title and subtitle to your plot, you can either use some custom HTML & CSS tricks or you just use the {reactablefmtr} package.

reactable(
  hawaiian_sales,
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(name = 'Month'),
    sales = colDef(name = 'Sales'),
    revenue = colDef(name = 'Revenue')
  )
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`

Format numbers

The numbers in the revenue column correspond to dollar amounts. We can format them by specifying a column format inside of colDef() with help from the colFormat() helper function.

reactable(
  hawaiian_sales,
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(name = 'Month'),
    sales = colDef(name = 'Sales'),
    revenue = colDef(
      name = 'Revenue',
      format = colFormat(currency = 'USD', separators = TRUE)
    )
  )
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`

Add groups

Now, I want to structure my tables into quarters. The easiest way to do that is to use the quarter column in our data set for grouping. The cool thing about {reactable} is that it’s really easy and the output becomes nicely interactive out of the box. All you have to do is set the groupBy argument.

reactable(
  hawaiian_sales,
  groupBy = 'quarter',
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(name = 'Month'),
    sales = colDef(name = 'Sales'),
    revenue = colDef(
      name = 'Revenue',
      format = colFormat(currency = 'USD', separators = TRUE)
    )
  )
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`

Add summaries

You can add group summaries by using the aggregate argument inside of colDef() and setting it to one of the built-in aggregate functions like "mean" or "sum". If you want to do something custom, you can do that, but then you will have to write a custom JavaScript function for that.

reactable(
  hawaiian_sales,
  groupBy = 'quarter',
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(name = 'Month'),
    sales = colDef(
      name = 'Sales',
      aggregate = 'sum'
    ),
    revenue = colDef(
      name = 'Revenue',
      format = colFormat(currency = 'USD', separators = TRUE),
      aggregate = 'sum'
    )
  )
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`

Make table searchable

If we wanted to make our table more interactive, we could make the month column filterable. That way, we can look for particular columns. In that case, it probably makes sense to have the groups unfolded by default.

reactable(
  hawaiian_sales,
  groupBy = 'quarter',
  defaultExpanded = TRUE, # Expand rows by default
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(
      name = 'Month',
      filterable = TRUE  # Make column filterable
    ),
    sales = colDef(
      name = 'Sales',
      aggregate = 'sum'
    ),
    revenue = colDef(
      name = 'Revenue',
      format = colFormat(currency = 'USD', separators = TRUE),
      aggregate = 'sum'
    )
  )
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`

Change row styling

Finally, to add a little bit more style and visual structure let us make the group rows blue. Let’s first try to change the theme() argument with the reactableTheme() helper function.

With that we can inject a bit of CSS to our table. The {htmltools} package makes it easy to combine multiple style instructions.

reactable(
  hawaiian_sales,
  groupBy = 'quarter',
  defaultExpanded = TRUE,
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(
      name = 'Month',
      filterable = TRUE 
    ),
    sales = colDef(
      name = 'Sales',
      aggregate = 'sum',
      footer =  JS("function(column, state) {
        let total = 0
        state.sortedData.forEach(function(row) {
          total += row[column.id]
        })
        return total
      }"),
    ),
    revenue = colDef(
      name = 'Revenue',
      format = colFormat(currency = 'USD', separators = TRUE),
      aggregate = 'sum',
      footer =  JS("function(column, state) {
        let total = 0
        state.sortedData.forEach(function(row) {
          total += row[column.id]
        })
        return total.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
      }")
    )
  ),
  theme = reactableTheme(
    rowGroupStyle = htmltools::css(
      background =  '#E7EDF3', 
      borderLeft = '2px solid #104E8B' 
    )
  )
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`

Unfortunately, this didn’t do what we want. This seems to target all cells in our table because they are all a row of some group. But to only highlight the header row of each group we will have to proceed differently.

For that, we need to write a JavaScript function that takes the rowInfo object as an argument and returns a JSON object with camelCased style properties. Thankfully, the reactable cookbook shows you exactly what you need.

reactable(
  hawaiian_sales,
  groupBy = 'quarter',
  defaultExpanded = TRUE,
  columns = list(
    quarter = colDef(name = 'Quarter'),
    month = colDef(
      name = 'Month',
      filterable = TRUE 
    ),
    sales = colDef(
      name = 'Sales',
      aggregate = 'sum',
      footer =  JS("function(column, state) {
        let total = 0
        state.sortedData.forEach(function(row) {
          total += row[column.id]
        })
        return total
      }"),
    ),
    revenue = colDef(
      name = 'Revenue',
      format = colFormat(currency = 'USD', separators = TRUE),
      aggregate = 'sum',
      footer =  JS("function(column, state) {
        let total = 0
        state.sortedData.forEach(function(row) {
          total += row[column.id]
        })
        return total.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
      }")
    )
  ),
  rowStyle = JS(
    "function(rowInfo) {
      if (rowInfo.level == 0) { // corresponds to row group
        return { 
          background: '#E7EDF3', 
          borderLeft: '2px solid #104E8B',
          fontWeight: 600
        }
      } 
    }"
  ),
) |> 
  reactablefmtr::add_title(
    title = 'Hawaiian Pizza Sales in 2015'
  ) |> 
  reactablefmtr::add_subtitle(
    subtitle = 'Based on the fake pizzaplace data from `{gt}`',
    font_weight = 'normal'
  )

Hawaiian Pizza Sales in 2015

Based on the fake pizzaplace data from `{gt}`


Enjoyed this blog post?

Here are three other ways I can help you:

3 Minutes Wednesdays

Every week, I share bite-sized R tips & tricks. Reading time less than 3 minutes. Delivered straight to your inbox. You can sign up for free weekly tips online.

Data Cleaning With R Master Class

This in-depth video course teaches you everything you need to know about becoming better & more efficient at cleaning up messy data. This includes Excel & JSON files, text data and working with times & dates. If you want to get better at data cleaning, check out the course page.

Insightful Data Visualizations for "Uncreative" R Users

This video course teaches you how to leverage {ggplot2} to make charts that communicate effectively without being a design expert. Course information can be found on the course page.