By Barbara Mikulášová, Senior Consultant
The idea behind buttons is quite simple, whether our users need to send an email report with the dashboard results, upload a new dataset, or rerun some operations – all they have to do is click a button. This makes buttons a powerful utility feature of any application.
This blog post showcases some subtle but very useful examples of action buttons from the {shiny} R package. Namely, we are going to take a closer looks at an Apply Changes button and a Reset Filters button.
Apply Changes
Reset Filters
But why these two buttons specifically when there are so many other options? The answer is that buttons of this type are highly requested by end users, especially if the purpose of the shiny application is to be a data exploration tool, and yet many programmers forget to include them.
The examples were written using the following packages:
The data used for the demo examples is a synthetic data set on speed tickets created with the {fakir} package with the following code:
```{r} # Load library library(fakir) # Generate synthetic dataset df <- fake_ticket_client(vol = 1000, seed = 2844) head(df) ```
actionButton(
inputId
```{r} # Load library library(shiny) # Define ui ui <- fluidPage( sidebarLayout( sidebarPanel( # Button 1 basic ---- actionButton( inputId = "basic_btn1", label = "Go" ) ), mainPanel( # Button 2 basic ---- actionButton( inputId = "basic_btn2", label = "Click me" ) ) ) ) # Define server function server <- function(input, output) { # Button 1 basic ---- observeEvent(input$basic_btn1,{ showNotification("Filter button was clicked") }) # Button 2 basic ---- observeEvent(input$basic_btn2,{ showNotification("Main button was clicked") }) } # Run the application shinyApp(ui = ui, server = server) ```
Here is what the ui looks like:
Depending on what you want the resulting action to be, you can choose from a variety of event handlers.
If you are using the version 1.6.0 of {shiny} and higher, it’s recommended to use a newly introduced bindEvent() function as your event handler, as opposed to observeEvent() or eventReactive() .
bindEvent()
observeEvent()
eventReactive()
An advantage of the bindEvent() function is that it is more robust than observeEvent() or eventReactive(). It understands from the context if your the code produces a reactive output or an effect, and it can also be used with render functions (e.g renderPlot())
renderPlot()
The first example be rewritten with bindEvent() as follows:
```{r} # Load library library(shiny) # Define ui ui <- fluidPage( sidebarLayout( sidebarPanel( # Button 1 basic ---- actionButton( inputId = "basic_btn1", label = "Go" ) ), mainPanel( # Button 2 basic ---- actionButton( inputId = "basic_btn2", label = "Click me" ) ) ) ) # Define server function server <- function(input, output) { # Button 1 basic ---- observe({ showNotification("Filter button was clicked") }) |> bindEvent(input$basic_btn1) # Button 2 basic ---- observe({ showNotification("Main button was clicked") })|> bindEvent(input$basic_btn2) } # Run the application shinyApp(ui = ui, server = server) ```
Notice how we used the base R pipe |> to pipe link the outcome of the trigger with its handler. In essence, observeEvent() is bindEvent(observe()) and eventReactive() is bindEvent(reactive()) .
|>
bindEvent(observe())
bindEvent(reactive())
Now that we have refreshed our memory on how to construct a simple button, let’s create the Apply Changes and Reset filters buttons.
Reset filters buttons
Have you ever tried to explore a particular subset of your data that involved filtering on 5 or 6 different variables? Watching the graphics constantly change as you are trying to adjust the settings is not a pleasant experience. Not to mention that the code repeatedly and needlessly renders the plots, which creates an issue when processing large amounts of data.
While reactive values are the holy grail of any {shiny} application, sometimes we need to define exactly when we want the execution chain to start in order to prevent the above mentioned problems. The Apply Changes button is one such example.
Let’s modify the above example to create a histogram of Age variable with two filters: Number of bins and Priority type. As my data will not change, I am not saving them as a reactive.
```{r} # Load library library(shiny) # load data df <- read.csv("www/fake_data.csv") # Define ui ui <- fluidPage( sidebarLayout( sidebarPanel( # Histogram bins ---- sliderInput( inputId = "bins", label = "Select bins:", min=10, max=40, value=15 ), # Priority filter ---- selectInput( inputId = "priority_f", label = "Select priority level:", choices = c("Silver", "Gold", "Platinium", "Bronze"), selected = "Bronze" ), # Apply button ---- actionButton( inputId = "apply_btn", label = "Apply Changes" ) ), mainPanel( # Histogram ---- plotOutput("hist") ) ) ) # Define server function server <- function(input, output) { output$hist <- renderPlot({ hist(df[df$priority == input$priority_f,"age"], breaks = input$bins) }) |> bindCache(input$bins, input$priority_f) |> bindEvent(input$apply_btn, ignoreNULL = FALSE) } # Run the application shinyApp(ui = ui, server = server) ```
The ignoreNull = False parameter ensures that the histogram is created using the default settings the first time the application is run. Otherwise, the user would have to press the Apply Changes button to generate it – and users rarely enjoy working with dashboards that are blank canvas upon opening.
ignoreNull = False
Users often want to return to the default filter settings to either compare their findings with predefined benchmark values, or to simply start their exploration from scratch. It is much easier to use a Reset Filters button than remembering the original filter settings or reloading the dashboard.
Since all input values are available the server side as read only values, we will use updateNumericInput() and updateTextInput() functions to change filter values on the client back to default.
updateNumericInput()
updateTextInput()
```{r} # Load library library(shiny) # load data df <- read.csv("www/fake_data.csv") # Define ui ui <- fluidPage( sidebarLayout( sidebarPanel( # Histogram bins ---- sliderInput( inputId = "bins", label = "Select bins:", min=10, max=40, value=15 ), # Priority filter ---- selectInput( inputId = "priority_f", label = "Select priority level:", choices = c("Silver", "Gold", "Platinium", "Bronze"), selected = "Bronze" ), # Reset button ---- actionButton( inputId = "reset_btn", label = "Reset filters" ) ), mainPanel( # Histogram ---- plotOutput("hist") ) ) ) # Define server function server <- function(input, output) { # Reset filter values observe({ updateNumericInput( session = getDefaultReactiveDomain(), "bins", value = 15 ) updateTextInput( session = getDefaultReactiveDomain(), "priority_f", value = "Bronze" ) }) |> bindEvent(input$reset_btn) output$hist <- renderPlot({ hist(df[df$priority == input$priority_f,"age"], breaks = input$bins) }) } # Run the application shinyApp(ui = ui, server = server) ```
Here is what our simple dashboard looks likes:
The default {shiny} buttons have a minimalistic look which does not suit every dashboard. Especially if you’re aiming to achieve a specific custom look reflecting the branding of your company.
The next section shows two common approaches to changing the appearance of {shiny} buttons.
Often in the pursuit of functionality, we neglect the visual aspects of our applications such as fonts, proper text justifications, colour palettes, and of course the look of the input controls and buttons!
The {shinyWidgets} package is a great resource for new shiny developers not yet feel confident in styling their input widgets from scratch using CSS but want to step up their user interface game.
This package is available on CRAN and contains a large collection of custom inputs widgets. To learn more about the package, see the documentation or browse the gallery showcasing the different widgets.
Notice that the package provides its own function for creating buttons actionBttn(). Be careful though, as it’s easy to confuse it with the original Shiny function actionButton(). Both these functions can coexist in your code together, as the functions from {shinyWidgets} do not mask the {shiny} functions.
actionBttn()
actionButton()
The easiest way to implement the buttons from {shinyWidgets} is to browse the gallery and note the values for the label, style, and color arguments, which determine the type and the style of the button you are using. The example below shows how to create a green pill button:
label
style
color
```{r} # Green pill shaped button actionBttn( inputId = "pill_button", label = "pill", style = "pill", color = "success" ) ```
You may have noticed that the colour of the button is determined by a keyword rather than a colour reference. Users have a set of 6 colours to choose from for widgets, which does not leave much room for colour customization and is one of the main disadvantages of this package.
However, there is a solution! We are going to discuss it in the following section.
The key to customizing the appearance of your application lies in CSS style sheets. CSS (Cascading Style Sheets) contain instructions about how HTML elements of your application should be displayed. The theory of how to construct a CSS file is beyond the scope of this blog post but there are many free online resources available. An honorary mention goes to the book Outstanding User Interfaces with Shiny by Kenton Russel, who provides practical introduction to CSS syntax and its applications in Shiny.
The below is an example of a simple CSS notation which customizes the button element of our application both for its default state and when users engage with the button by hovering a cursor above it. As we are not targeting specific elements by their id, the rules will be applied to all of the application buttons.
```{CSS} /* General button look*/ .btn-default{ color: #344585; border-color: #344585; background-color:transparent; font-family: system-ui; font-size: 16px; padding: 5px 9px; outline: none; margin: 0; cursor: pointer; border-radius: 4px; transition: all .3s cubic-bezier(.02,.01,.47,1); position: relative; opacity: .70; } .btn-default:hover{ color: white; background-color:#3b3765 ; border-color: #3b3765; } ```
The most common method of using CSS file(s) in RShiny application is to save them in the special www folder, which is where Shiny looks for external resources such images, videos or other scripts. The file is then referenced as an external resource by using the Shiny HTML tags. Include the tags in the fluidPage() function which constructs the ui part of the application. See the example below:
www
fluidPage()
```{r} ui <- fluidPage( # CSS Style sheet ---- tags$head( tags$link(rel = "stylesheet", type="text/css", href="buttons_style.css") ), # Rest of the ui code ) ```
This is how the style sheet changed the look of our buttons:
As you can see, the inclusion of CSS files opens a whole new world of customization for your Shiny application.
In summary, we looked at the importance of buttons, clever buttons, and beautiful buttons in user interface of Shiny applications. Buttons give users the ability to trigger a variety of events in the application. When implemented properly, they contribute to the seamless functionality that users expect. However, having practical buttons does not mean they cannot be beautiful too. To enhance the appearance, we can either use already stylized input widgets from the {shinyWidgets} package or, for the more adventurous RShiny developers, we can define our own style sheets with CSS.
It is time to elevate your {shiny} dashboards functionality and design with clever use of (not only) beautiful buttons.