Interactive filters in tables with reactable

Tables
JavaScript
reactable is a great package to make interactive tables in R. And you can make your tables even more interactive by combining them with a tiny bit of JavaScript.
Author

Albert Rapp

Published

January 21, 2024

The reactable package is a powerful tool that simplifies the creation of interactive HTML tables. While it’s quite straightforward to use, it lacks built-in support for dynamic interactions based on the content of a dataset. To show you what I mean have a look at this table. It shows you the five heaviest penguins for each species in the palmerpenguins dataset.

library(tidyverse)
library(reactable)

penguins_dat <- palmerpenguins::penguins |> 
  slice_max(body_mass_g, n = 5, by = species) |>
  select(species, island, body_mass_g) 

penguins_dat |> 
  reactable(
    columns = list(
      species = colDef(name = "Species"),
      island = colDef(name = "Island"),
      body_mass_g = colDef(name = "Weight (g)")
    )
  )


By default this table can be sorted by the values in each column. Just click on the table names and see how everything gets sorted. But what if you want to filter the table by a specific species or island? This could look something like this.


Notice how the table has a dropdown menu for each of the species and island columns. The weight column is filterable too but it has a text search filter instead of a dropdown menu. To create this table, you have to resort to add a bit of JavaScript as well as HTML elements with a bit of help from the htmltools package. In this blog post, I’ll show you how that works. And if you’re interested in watching the video version of this blog post, you can find it here:

Make table filterable

The first step to create our desired table is to make our original reactable table filterable. It turns out that reactable actually has a standard option for that.

penguins_dat |>
  reactable(
    columns = list(
      species = colDef(name = "Species"),
      island = colDef(name = "Island"),
      body_mass_g = colDef(name = "Weight (g)")
    ),
    filterable = TRUE # Make a change here
  )

Create a dropdown menu

Next, we have to define a function that creates the dropdown menu. We will stick this one into the filterInput argument of the colDef() function. As per the documentation of colDef(), the filter function needs to have two arguments: values and name. These arguments correspond to the values of a column and its name. Now, to create a dropdown menu we use the tags$select() function from the htmltools package.

library(htmltools)
tags$select()

This doesn’t look great. By default, tags$select() will give us an empty dropdown menu. So let’s add some options to it by using the tags$option() function. For example, for the species we could do something like this.

tags$select(
  tags$option(value = "", "All"),
  tags$option(value = "Adelie", "Adelie"),
  tags$option(value = "Chinstrap", "Chinstrap"),
  tags$option(value = "Gentoo", "Gentoo")
)

Nice, this looks pretty decent. But of course this is pretty specific to the species column. Thus, let us use map() from the purrr package to iterate over the unique values of the species column and create an option for each of them.

tags$select(
  tags$option(value = "", "All"),
  purrr::map(unique(penguins_dat$species), tags$option)
)

How is this more general, you ask? Well, we can use this code for any column. Just replace the penguins_dat$species part with values and we’re good to go. You know, because the values argument of the filter function corresponds to the values of a column.

filter_fct <- function(values, name) {
  tags$select(
    tags$option(value = "", "All"),
    purrr::map(unique(values), tags$option)
  )
}
filter_fct(penguins_dat$species, "species")

Add reactivity

Notice how the name argument doesn’t do anything here. So does this dropdown menu. We might change the selected value of the dropdown menu but nothing happens. That’s because we haven’t told anybody what to do when the selection changes. But we can do that now. Just insert the onchange argument into the tags$select() function.

filter_fct <- function(values, name) {
  tags$select(
    tags$option(value = "", "All"),
    purrr::map(unique(values), tags$option),
    onchange = glue::glue("alert('The {name} column changed! Hooray!')")
  )
}
filter_fct(penguins_dat$species, "species")

Here I’ve use a tiny bit of JavaScript to show a text alert. Whenever you change the selected value of the dropdown menu, the alert will say “The species column changed! Hooray!”. But only because I’ve used the glue package to insert the name argument into the JavaScript code.

Change the underlying data with JS

To summarize, we have

  • an R function that
  • creates JS code that is
  • executed when the dropdown menu changes.

Pretty dope if you think about it. Anyway, now it’s time to figure out what the JS code to filter the data is. Let me just tell you: The code for that uses the Reactable.setFilter() function. How do I know? Well, I found this out in the INSAAAANELY good documentation of the reactable package.

Now, what we need to understand is that this function takes three arguments:

  • the table id,
  • the column name and the
  • value to filter by.

This means our table needs to have an id. Time to modify the table:

penguins_dat |>
  reactable(
    columns = list(
      species = colDef(name = "Species"),
      island = colDef(name = "Island"),
      body_mass_g = colDef(name = "Weight (g)")
    ),
    elementId = "my-tbl", # Add an id here
    filterable = TRUE
  )

Now, what we can do is to use the glue package again to insert the table id and the column name into the JS Reactable.setFilter() function.

filter_fct <- function(values, name) {
  tags$select(
    tags$option(value = "", "All"),
    purrr::map(unique(values), tags$option),
    onchange = glue::glue(
      "Reactable.setFilter(
        'my-tbl', 
        '{name}', 
        event.target.value  // This is the value of the dropdown menu
      )"
    )
  )
}
filter_fct(penguins_dat$species, "species")

Hoooray, we did it! But wait! Why? Are you surprised that we’re already done? Well, try it out. Change the dropdown menu and see how the table changes. The dropdown menu does not have to be inserted into the colDef() function for all of this to work. The JS code that targets the table with the id my-tbl will be executed regardless of where the dropdown menu is (as long as it’s on the same web page).

Still, the table looks probably nicer if we put the dropdown menu into the colDef() function. So let’s do that. But since we create another table, we need to change the id of the table to my-tbl2. Similarly, we will have to adjust the name inside of filter_fct().

filter_fct <- function(values, name) {
  tags$select(
    tags$option(value = "", "All"),
    purrr::map(unique(values), tags$option),
    onchange = glue::glue(
      "Reactable.setFilter(
        'my-tbl2', // This is the new ID
        '{name}', 
        event.target.value  
      )"
    )
  )
}
penguins_dat |>
  reactable(
    columns = list(
      species = colDef(
        name = "Species",
        filterInput = filter_fct
      ),
      island = colDef(
        name = "Island",
        filterInput = filter_fct
      ),
      body_mass_g = colDef(name = "Weight (g)")
    ),
    elementId = "my-tbl2", # New id here
    filterable = TRUE
  )

Nice! We can easily filter our tables now. But we should probably make this look nicer. So let’s add two short lines of CSS code to make the dropdown menus look nicer.

filter_fct <- function(values, name) {
  tags$select(
    tags$option(value = "", "All"),
    purrr::map(unique(values), tags$option),
    onchange = glue::glue(
      "Reactable.setFilter(
        'my-tbl3', // Modify id again
        '{name}', 
        event.target.value  
      )"
    ),
    style = "width: 100%; height: 28px;" # Add this line
  )
}
penguins_dat |>
  reactable(
    columns = list(
      species = colDef(
        name = "Species",
        filterInput = filter_fct
      ),
      island = colDef(
        name = "Island",
        filterInput = filter_fct
      ),
      body_mass_g = colDef(name = "Weight (g)")
    ),
    elementId = "my-tbl3", # Modify id again
    filterable = TRUE
  )

Conclusion

And there you have it, a table that can be filtered by the values of a column using a dropdown menu. This is a great way to make your tables more interactive. Here, we used functions from the htmltools package to create the dropdown menu. Alternatively, we could also use Observable JS for that. But that’s a story for another time. In the meantime, you may want to check out some of my other content:


Stay in touch

If you enjoyed this post, then you may also like my weekly 3-minute newsletter. Every week, I share insights on data visualization, statistics and Shiny web app development. Reading time: 3 minutes or less. You can check it out via this link.

You can also support my work with a coffee