Chapter 9 Dynamic UI

So far, we’ve seen a clean separation between the user interface and the server function. The user interface is defined statically, when the app is launched. That means it can’t respond to anything that happens in the app. In this chapter, you’ll learn how to create dynamic user interfaces, using code run in the server function. There are three key techniques for creating dynamic user interfaces:

  • Using the update functions to modify the parameters of input controls.

  • Using tabsetPanel() to conditionally show and hide parts of the user interface.

  • Using uiOutput() and renderUI() to generate selected parts of the user interface with code.

These three tools give you considerable power to respond to the user by modifying inputs and outputs. I’ll demonstrate some of the more useful ways in which you can apply them, but ultimately, you are only constrained by your creativity. That said, these tools can make your app substantially more difficult to reason about, so deploy them sparingly, and always strive for the simplest technique that solves your problem.

9.1 Updating inputs

We’ll begin with a simple technique that allows you to modify an input after it has been created. Every input control, e.g. textInput(), is paired with an update function, e.g. updateTextInput(), that allows you to modify the control after it has been created. The update funtions look a little different to other Shiny functions: they all takes the current session as the first argument, and the name of the input (as a string) as the second. The remaining arguments match the arguments to the input constructor.

Take the example in the code below, with the results shown in Figure 9.1. The app has two inputs that control the range (the min and max) of another input, a slider. The key idea is to use observeEvent()20 to trigger updateNumericInput() whenever the min or max inputs change.

The app on load (left), after increasing max (middle), and then decreasing min (right). See live at <https://hadley.shinyapps.io/ms-update-basics>.The app on load (left), after increasing max (middle), and then decreasing min (right). See live at <https://hadley.shinyapps.io/ms-update-basics>.The app on load (left), after increasing max (middle), and then decreasing min (right). See live at <https://hadley.shinyapps.io/ms-update-basics>.

Figure 9.1: The app on load (left), after increasing max (middle), and then decreasing min (right). See live at https://hadley.shinyapps.io/ms-update-basics.

To help you get the hang of the update functions, I’ll show a couple more simple examples. Then we’ll dive into a more complicated case study using hierarchical select boxes, and I’ll finish off by discussing circular references and the related problem of multiple sources of truth.

9.1.1 Simple uses

The simplest uses of the update functions are to provide small conveniences for the user. For example, maybe you want to make it easy to reset parameters back to their initial value. The following snippet shows how you might combine an actionButton(), observeEvent() and updateNumericInput(), with the output shown in Figure 9.2.

The app on load (left), after dragging some sliders (middle), then clicking reset (right). See live at <https://hadley.shinyapps.io/ms-update-reset>.The app on load (left), after dragging some sliders (middle), then clicking reset (right). See live at <https://hadley.shinyapps.io/ms-update-reset>.The app on load (left), after dragging some sliders (middle), then clicking reset (right). See live at <https://hadley.shinyapps.io/ms-update-reset>.

Figure 9.2: The app on load (left), after dragging some sliders (middle), then clicking reset (right). See live at https://hadley.shinyapps.io/ms-update-reset.

A similar application is to tweak the text of an action button so you know exactly what it’s going to do. Figure 9.3 shows the results of the code below.

The app on load (left), after setting simulations to 1 (middle), then settting simulations to 10 (right). See live at <https://hadley.shinyapps.io/ms-update-button>.The app on load (left), after setting simulations to 1 (middle), then settting simulations to 10 (right). See live at <https://hadley.shinyapps.io/ms-update-button>.The app on load (left), after setting simulations to 1 (middle), then settting simulations to 10 (right). See live at <https://hadley.shinyapps.io/ms-update-button>.

Figure 9.3: The app on load (left), after setting simulations to 1 (middle), then settting simulations to 10 (right). See live at https://hadley.shinyapps.io/ms-update-button.

There are many ways to use update functions in this way; be on the look out for ways to give more information to the user when you are working on sophisticated applications. A particularly important application is making it easier to select from a long list of possible options, through step-by-step filtering. That’s often a problem for “hierarchical select boxes”.

9.1.2 Hierarchical select boxes

A more complicated, but particularly useful, application of the update functions is to allow interactive drill down across multiple categories. I’ll illustrate their usage with some imaginary data for a sales dashboard that comes from https://www.kaggle.com/kyanyoga/sample-sales-data.

For our purposes, I’m going to focus on a natural hierarchy in the data:

  • Each territory contains customers.
  • Each customer has multiple orders.
  • Each order contains rows.

I want to create a user interface where you can:

  • Select a territory to see all customers.
  • Select a customer to see all orders.
  • Select an order to see the underlying rows.

The essence of the UI is simple: I’ll create three select boxes and one output table. The choices for the customername and ordernumber select boxes will be dynamically generated, so I set choices = NULL.

In the server function, I work top-down:

  1. I create a reactive, territory(), that contains the rows from sales that match the selected territory.

  2. Whenever territory() changes, I update the list of choices in the input$customername select box.

  3. I create another reactive, customer(), that contains the rows from territory() that match the selected customer.

  4. Whenever customer() changes, I update the list of choices in the the input$ordernumber select box.

  5. I display the selected orders in output$data.

I select "EMEA" (left), then "Lyon Souveniers" (middle), then (right) look at the orders. See live at <https://hadley.shinyapps.io/ms-update-nested>.I select "EMEA" (left), then "Lyon Souveniers" (middle), then (right) look at the orders. See live at <https://hadley.shinyapps.io/ms-update-nested>.I select "EMEA" (left), then "Lyon Souveniers" (middle), then (right) look at the orders. See live at <https://hadley.shinyapps.io/ms-update-nested>.

Figure 9.4: I select “EMEA” (left), then “Lyon Souveniers” (middle), then (right) look at the orders. See live at https://hadley.shinyapps.io/ms-update-nested.

Try out this simple example at https://hadley.shinyapps.io/ms-update-nested, or see a more fully fleshed out application at https://github.com/hadley/mastering-shiny/tree/master/sales-dashboard.

9.1.3 Circular references

There’s an important issue we need to discuss if you want to use update functions to change the value21 of inputs. From Shiny’s perspectve, using an update function to modify value is no different to the user modifying the value by clicking or typing. That means an update function can trigger reactive updates in exactly the same way that a human can. This means that you are now stepping outside of the bounds of pure reactive programming, and you need to start worrying about circular references and infinite loops.

For example, take the following simple app. It contains a single input control and a observer that increments its value by one. Every time updateNumericInput() runs, it changes input$n, causing updateNumericInput() to run again, so the app gets stuck in an infinite loop constantly increasing the value of input$n.

You’re unlikely to create such an obvious problem in your own app, but it can crop up if you updating multiple controls that depend on one another, as in the next example.

9.1.5 Exercises

  1. Complete the user interface below with a server function that updates input$date so that you can only select dates in input$year.

  2. Complete the user interface below with a server function that updates input$county choices based on input$state. For an added challenge, also change the label from “County” to “Parrish” for Louisana and “Borrough” for “Alaska”.

  3. Complete the user interface below with a server function that updates input$country choices based on the input$continent. Use output$data to display all matching rows.

  4. Extend the previous app so that you can also choose to select no continent, and hence see all countries. You’ll need to add "" to the list of choices, and then handle that specially when filtering.

  5. What is at the heart of the problem described at https://community.rstudio.com/t/29307?

9.2 Dynamic visibility

The next step up in complexity is to selectively show and hide parts of the UI. You’ll learn a number of sophisticated approaches later, once you’ve learned a little JS and CSS, but there’s a useful technique that you can use now: concealing optional UI in a tabset. This is a clever hack that allows you to show and hide UI as needed, without having to re-generate it from scratch (as you’ll learn in the next section).

Currently22 you need a smidgen of CSS to make this technique work: tags$style("#switcher { display:none; }"). If you adapt this for your own code make sure to repace #switcher with # followed the id of your the tabsetPanel().

Selecting panel1 (left), then panel2 (middle), then panel3 (right). See live at <https://hadley.shinyapps.io/ms-dynamic-panels>.Selecting panel1 (left), then panel2 (middle), then panel3 (right). See live at <https://hadley.shinyapps.io/ms-dynamic-panels>.Selecting panel1 (left), then panel2 (middle), then panel3 (right). See live at <https://hadley.shinyapps.io/ms-dynamic-panels>.

Figure 9.5: Selecting panel1 (left), then panel2 (middle), then panel3 (right). See live at https://hadley.shinyapps.io/ms-dynamic-panels.

There are two main ideas here:

  • Use tabset panel with hidden tabs.
  • Use updateTabsetPanel() to switch tabs from the server.

This is a simple idea, but when combined with a little creativity, it gives you a considerable amount of power. The following two sections illustrate give a couple of small examples of how you might use it in practice.

9.2.1 Conditional UI

Imagine that you want an app that allows the user to simulate from the normal, uniform, and exponential distributions. Each distribution has different parameters, so we’ll need some way to show different controls for different distributions. Here, I’ll put the unique user interface for each distribution in its on tabPanel(), and then arrange the three tabs into a tabsetPanel().

I’ll then embed that inside a fuller UI which allows the user to pick the number of samples and shows a histogram of the results:

Note that I’ve carefully matched the choices in input$dist to the names of the tab panels. That makes it easy to write the observeEvent() code below that automatically switches controls when the distribution changes. The rest of the app uses techniques that you’re already familiar with. See the final result in Figure 9.6.

Results for normal (left), uniform (middle), and exponential (right) distributions. See live at <https://hadley.shinyapps.io/ms-dynamic-conditional>.Results for normal (left), uniform (middle), and exponential (right) distributions. See live at <https://hadley.shinyapps.io/ms-dynamic-conditional>.Results for normal (left), uniform (middle), and exponential (right) distributions. See live at <https://hadley.shinyapps.io/ms-dynamic-conditional>.

Figure 9.6: Results for normal (left), uniform (middle), and exponential (right) distributions. See live at https://hadley.shinyapps.io/ms-dynamic-conditional.

9.2.3 Exercises

9.3 Creating UI with code

Sometimes neither of the techniques described above gives you the level of dynamism that you need. There’s one last technique that gives you the ability to create any controls (both inputs and outputs) with code in the server function. You’ve always created your UI with code; this technique gives you the ability to re-generate while the app is running, not just define it when the app starts.

This technique has two components:

  • You use uiOutput() to insert a placeholder in your user interface. This code is run when your app launches and it leaves a “hole” that your server code can later fill in.

  • You use renderUI() to fill in the placeholder with UI generated in the server function.

Here’s a simple example to illustrate the basic idea. It dynamically creates a different type of input control depending on an input. The resulting app is shown in Figure ??.

App after setting value to 5 (left), then changing type to numeric (middle), then label to 'my label'. See live at <https://hadley.shinyapps.io/ms-render-simple>.App after setting value to 5 (left), then changing type to numeric (middle), then label to 'my label'. See live at <https://hadley.shinyapps.io/ms-render-simple>.App after setting value to 5 (left), then changing type to numeric (middle), then label to 'my label'. See live at <https://hadley.shinyapps.io/ms-render-simple>.

Figure 9.7: App after setting value to 5 (left), then changing type to numeric (middle), then label to ‘my label’. See live at https://hadley.shinyapps.io/ms-render-simple.

If you run this code yourself, you’ll notice that it takes a fraction of a second to appear after the app loads. That’s because it’s reactive: the app must load, trigger a reactive event, which calls the server function, yielding HTML to insert into the page. This is one of the downsides of renderUI(); relying on it too much can create a laggy UI. For good performance, strive to keep fixed as much of the user interface as possible, using the techniques described earlier in the chapter.

There’s one other problem with this approach: when you change controls, you lose the currently selected value. Maintaining existing state is one of the big challenges of creating UI with code. This is one reason that selectively showing and hiding UI (as above) is a better approach if works for you - because you’re not destroying and recreating the controls, you don’t need to do anything to presrve the values. In many cases, we can fix the problem by setting the value of the new input to the current value of the existing control:

(I’ve removed the min and max arguments make it easier to fit everything on one line; you’d still need them in real code.)

The use of isolate() is important. We’ll come back to exactly why it’s needed in Chapter XYZ, but it ensures that we don’t create a reactive dependency that would mean this code is re-run every time input$dynamic changes. We only want it to change when input$type or inpu$label changes.

Dynamic UI is most useful when you are generating an arbitrary number or type of controls. That means that you’ll be generating UI with code, and I recommend using functional programming for this sort of task. You’re certainly free to use base lapply() and Reduce() functions for this pupose, but I’m going to use purrr::map() and purrr::reduce(), because I find them a little easier to work with:

If you’re not familiar with the basic map() and reduce() of functional programming, you might want to take a brief detour to read https://adv-r.hadley.nz/functionals.html before continuing.

9.3.1 Multiple controls

An important use of renderUI() is when you need to create an arbitrary number of controls. For example, imagine you’d like the user to be able to supply their own palette of colours. They’ll first specify how many colours they want, and then supply a value for each colour. The ui is pretty simple: we have a numericInput() that controls the number of inputs, a uiOutput() where the generated text boxes will go, and a textOutput() that demonstrates that we’ve plumbed everything together correctly.

There are three key ideas in the server function:

  • I create a reactive, col_names(), that generates a character vector giving the name of each of the colour inputs I’m about to generate.

  • I create the text boxes by using map() to create one textInput() for input in col_names(). output$col <- renderUI() inserts these textboxes in the UI placeholder that I created earlier.

  • To generate the output, I need to use use a new trick. So far we’ve always accessed the components of input with $, e.g. input$col1. But here we have the input names in a character vector, like var <- "col1". $ no longer works in this scenario, so we need to swich to [[, i.e. input[[var]]. I use map_chr() to collect all values into a character vector, and display that in output$pallete.

You can see the results in Figure 9.8.

App on load (left), after setting n to 3 (middle), then entering some colours (right). See live at <https://hadley.shinyapps.io/ms-render-palette>.App on load (left), after setting n to 3 (middle), then entering some colours (right). See live at <https://hadley.shinyapps.io/ms-render-palette>.App on load (left), after setting n to 3 (middle), then entering some colours (right). See live at <https://hadley.shinyapps.io/ms-render-palette>.

Figure 9.8: App on load (left), after setting n to 3 (middle), then entering some colours (right). See live at https://hadley.shinyapps.io/ms-render-palette.

If you run this app, you’ll discover a really annoying behaviour: whenever you change the number of colours, all the data you’ve entered disappears. We can fix this problem by using the same technique as before: setting value to the (isolated) current value. I’ll also tweak the appearance to look a little nicer, including displaying the selected colours in a plot. Sample screenshots are shown in Figure 9.9.

Filling out the colours of the rainbow (left), then reducing the number of colours to 3 (right); note that the existing colours are preserved. See live at <https://hadley.shinyapps.io/ms-render-palette-full>.Filling out the colours of the rainbow (left), then reducing the number of colours to 3 (right); note that the existing colours are preserved. See live at <https://hadley.shinyapps.io/ms-render-palette-full>.

Figure 9.9: Filling out the colours of the rainbow (left), then reducing the number of colours to 3 (right); note that the existing colours are preserved. See live at https://hadley.shinyapps.io/ms-render-palette-full.

9.3.2 Dynamic filtering

To finish off the chapter, I’m going to create an app that lets you dynamically filter any data frame. Each numeric input will get a range slider and each factor input will get a multi-select. i.e. if a data frame has 3 continuous variables and 2 factors, I’ll generate an app with 3 sliders and 2 select boxes.

I’ll start with a function that creates the UI for a single variable. It’ll return a range slider for numeric inputs, a multi-select for factor inputs, and NULL (nothing) for all other types.

And then I’ll write the server side equivalent of this function: it takes the variable and value of the input control, and returns a logical vector saying whether or not to include each observation. I return a logical vector here because it’ll make it easy to combine the results from multiple columns.

I can then use these functions “by hand” to generate a simple filtering UI for the iris dataset:

Simple filter interface for the iris dataset

Figure 9.10: Simple filter interface for the iris dataset

You might notice that I got sick of copying and pasting so that app only works with three columns. I can make it work with all the columns by using a little functional programming:

  • In ui use map() to generate one control for each variable.

  • In server(), I use map() to generate the selection vector for each variable. Then I use reduce() to take the logical vector for each variable and combine into a single logical vector by &-ing each vector together.

Using functional programming to build filtering app for the `iris` dataset

Figure 9.11: Using functional programming to build filtering app for the iris dataset

From there, it’s a simple generalisation to work with any data frame. Here I’ll illustrate it using the data frames in the datasets package, but you can easily imagine how you might extend this to user uploaded data. See the result in Figure ??.

A dynamic user interface automatically generated from the fields of the selected dataset. See live at <https://hadley.shinyapps.io/ms-filtering-final>.

Figure 9.12: A dynamic user interface automatically generated from the fields of the selected dataset. See live at https://hadley.shinyapps.io/ms-filtering-final.

9.3.3 Exercises

  1. Take this very simple app based on the initial example in the chapter:

    How could you instead implement it using dynamic visibility? If you implement dynamic visiblity, how could you keep the values in sync when you change the controls?

  2. Add support for date and date-time columns make_ui() and filter_var().

  3. (Advanced) If you know the S3 OOP system, consider how you could replace the if blocks in make_ui() and filter_var() with generic functions.

  4. (Hard) Make a wizard that allows the user to upload their own dataset.
    The first page should handle the upload. The second should handle reading it, providing one drop down for each variable that lets the user select the column type. The third page should provide some way to get a summary of the dataset.


  1. Note that I’ve used observeEvent() here, although observe() would also work and would be more concise. I generally prefer observeEvent() because its arguments cleanly separate the event you’re listening for from the action you want to take in response.

  2. This is generally only a concern when you are changing the value, but be some other parameters can change the value indirectly. For example, if you modify the choices for selectInput(), or min and max for sliderInput(), the current value will be modified if it’s no longer in the allowed set of values.

  3. Hopefully, it will be built into tabsetPanel() in the future; follow https://github.com/rstudio/shiny/issues/2680 for details.