Chapter 3 Basic UI

3.1 Introduction

Now that you’ve got a basic app under your belt, we’re going to explore the details that make it tick. As you saw in the previous chapter, Shiny encourages separation of the code that generates your user interface (the front end) from the code that drives your app’s behaviour (the backend). In this chapter, we’ll dive deeper into the front end and explore the HTML inputs, outputs, and layouts provided by Shiny.

Learning more about frontend will allow you to generate visually compelling, but simple apps. In the next chapter, you’ll learn more about the reactivity that powers Shiny’s backend, allowing you to create richer responses to interaction.

As usual, we’ll begin by loading the shiny package:

3.2 Inputs

As we saw in the previous chapter, you use functions like sliderInput(), selectInput(), textInput(), and numericInput() to insert input controls into your UI specification. Now we’ll discuss the common structure that underlies all input functions and give a quick overview of the inputs built into Shiny.

3.2.1 Common structure

All input functions have the same first argument: inputId. This is the identifier used to connect the front end with the back end: if your UI has an input with ID "name", the server function will access it with input$name.

The inputId has two constraints:

  • It must be a simple string that contains only letters, numbers, and underscores (no spaces, dashes, periods, or other special characters allowed!). Name it like you would name a variable in R.

  • It must be unique. If it’s not unique, you’ll have no way to refer to this control in your server function!

Most input functions have a second parameter called label. This is used to create a human-readable label for the control. Shiny doesn’t place any restrictions on this string, but you’ll need to carefully think about it to make sure that your app is usable by humans! The third parameter is typically value, which, where possible, lets you set the default value. The remaining parameters are unique to the control.

When creating an input, I recommend supplying the inputId and label arguments by position, and all other arguments by name:

The following sections describe the inputs built in to Shiny, loosely grouped according to the type of control they create. The goal is to give you a rapid overview of your options, not to exhaustively describe all the arguments. I’ll show the most important parameters for each control below, but you’ll need to read the documentation to get the full details.

3.2.2 Free text

Collect small amounts of text with textInput(), passwords with passwordInput()2, and paragraphs of text with textAreaInput().

If you want to ensure that the text has certain properties you can use validate(), which we’ll come back to in Chapter 7.

3.2.3 Numeric inputs

To collect numeric values, create a slider with sliderInput() or a constrained textbox with numericInput(). If you supply a length-2 numeric vector for the default value of sliderInput(), you get a “range” slider with two ends.

Generally, I recommend only using sliders for small ranges, or cases where the precise value is not so important. Attempting to precisely select a number on a small slider is an exercise in frustration!

Sliders are extremely customisable and there are many ways to tweak their appearance. See ?sliderInput and https://shiny.rstudio.com/articles/sliders.html for more details.

3.2.4 Dates

Collect a single day with dateInput() or a range of two days with dateRangeInput(). These provide a convenient calendar picker, and additional arguments like datesdisabled and daysofweekdisabled allow you to restrict the set of valid inputs.

Date format, language, and the day on which the week starts defaults to US standards. If you are creating an app with an international audience, set format, language, and weekstart so that the dates are natural to your users.

3.2.5 Limited choices

There are two different approaches to allow the user to choose from a prespecified set of options: selectInput() and radioButtons().

Radio buttons have two nice features: they show all possible options, making them suitable for short lists, and via the choiceNames/choiceValues arguments, they can display options other than plain text.

Dropdowns created with selectInput() take up the same amount of space, regardless of the number of options, making them more suitable for longer options. You can also set multiple = TRUE to allow the user to select multiple elements.

There’s no way to select multiple values with radio buttons, but there’s an alternative that’s conceptually similar: checkboxGroupInput().

If you want a single checkbox for a single yes/no question, use checkboxInput():

3.2.6 File uploads

Allow the user to upload a file with fileInput():

fileInput() requires special handling on the server side, and is discussed in detail in Chapter 8.

3.2.7 Action buttons

Let the user perform an action with actionButton() or actionLink(). These are most naturally paired with observeEvent() or eventReactive() in the server function; I haven’t discussed these functions yet, but we’ll come back to them in the next chapter.

3.2.8 Exercises

  1. When space is at a premium, it’s useful to label text boxes using a placeholder that appears inside the text entry area. How do you call textInput() to generate the UI below?

  2. Carefully read the documentation for sliderInput() to figure out how to create a date slider, as shown below.

  3. If you have a moderately long list, it’s useful to create sub-headings that break the list up into pieces. Read the documentation for selectInput() to figure out how. (Hint: the underlying HTML is called <optgroup>.)

  4. Create a slider input to select values between 0 and 100 where the interval between each selectable value on the slider is 5. Then, add animation to the input widget so when the user presses play the input widget scrolls through automatically.

  5. Using the following numeric input box the user can enter any value between 0 and 1000. What is the purpose of the step argument in this widget?

3.3 Outputs

Outputs in the UI create placeholders that are later filled by the server function. Like inputs, outputs take a unique ID as their first argument: if your UI specification creates an output with ID "plot", you’ll access it in the server function with output$plot.

Each output function on the front end is coupled with a render function in the back end. There are three main types of output, corresponding to the three things you usually include in a report: text, tables, and plots. The following sections show you the basics of the output functions on the front end, along with the corresponding render functions in the back end.

3.3.1 Text

Output regular text with textOutput() and fixed code and console output with verbatimTextOutput().

Note that the {} are not required in render functions, unless you need to run multiple lines of code. You could also write the server function more compactly. I think this is generally good style as you should do as little computation in your render functions as possible.

Note that there are two render functions that can be used with either of the text output functions:

  • renderText() which displays text returned by the code.
  • renderPrint() which displays text printed by the code.

To help understand the difference, examine the following function. It prints a and b, and returns "c". A function can print multiple things, but can only return a single value.

3.3.2 Tables

There are two options for displaying data frames in tables:

  • tableOutput() and renderTable() render a static table of data, showing all the data at once.

  • dataTableOutput() and renderDataTable() render a dynamic table, showing a fixed number of rows along with controls to change which rows are visible.

tableOutput() is most useful for small, fixed summaries (e.g. model coefficients); dataTableOutput() is most appropriate if you want to expose a complete data frame to the user.

3.3.3 Plots

You can display any type of R graphic (base, ggplot2, or otherwise) with plotOutput() and renderPlot():

By default, plotOutput() will take up the full width of its container (more on that shortly), and will be 400 pixels high. You can override these defaults with the height and width arguments.

Plots are special because they are outputs that can also act as inputs. plotOutput() has a number of arguments like click, dblclick, and hover. If you pass these a string, like click = "plot_click", they’ll create a reactive input (input$plot_click) that you can use to handle user interaction on the plot. We’ll come back to interactive plots in Shiny in Chapter XYZ.

3.3.4 Downloads

You can let the user download a file with downloadButton() or downloadLink(). These require new techniques in the server function, so we’ll come back to that in Chapter 8.

3.3.5 Exercises

  1. Re-create the Shiny app from the plots section, this time setting height to 300px and width to 700px.

  2. Add an additional plot to the right of the existing plot, and size it so that each plot takes up half the width of the app.

  3. Update the options for renderDataTable() below so that the table is displayed, but nothing else, i.e. remove the search, ordering, and filtering commands. You’ll need to read ?renderDataTable and review the options at https://datatables.net/reference/option/.

3.4 Layouts

Now that you know how to create a full range of inputs and outputs, you need to be able to arrange them on the page. That’s the job of the layout functions, which provide the high-level visual structure of an app. Here we’ll focus on fluidPage(), which provides the layout style used by most apps. In future chapters you’ll learn about other layout families like dashboards and dialog boxes.

3.4.1 Overview

Layouts are created by a hierarchy of function calls, where the hierarchy in R matches the hierarchy in the output. When you see complex layout code like this:

First skim it by focusing on the hierarchy of the function calls:

Even without knowing anything about the layout functions you can read the function names to guess what this app is going to look like. You might imagine that this code swill generate a classic app design: a title bar at top, followed by a sidebar (containing a slider), with the main panel containing a plot.

3.4.2 Page functions

The most important, but least interesting, layout function is fluidPage(). You’ve seen it in every example above, because we use it to put multiple inputs or outputs into a single app. What happens if you use fluidPage() by itself?

It looks very boring because there’s no content, but behind the scenes, fluidPage() is doing a lot of work. The page function sets up all the HTML, CSS, and JS that Shiny needs. fluidPage() uses a layout system called Bootstrap, https://getbootstrap.com, that provides attractive defaults3. Later on, in Chapter XYZ, we’ll talk about how you can use a little knowledge of Bootstrap to gain greater control of the visual appearance of your app in order to make your app look more polished or match your corporate style guide.

Technically, fluidPage() is all you need for an app, because you can put inputs and outputs directly inside of it. But while this is fine for learning the basics of Shiny, dumping all the inputs and outputs in one place doesn’t look very good, so you need to learn more layout functions. Here I’ll introduce you to two common structures, a page with sidebar and a multirow app, and then we’ll finish off with a quick discussion of themes.

3.4.3 Page with sidebar

sidebarLayout(), along with titlePanel(), sidebarLayout(), sidebarPanel(), and mainPanel(), makes it easy to create a two-column layout with inputs on the left and outputs on the right. The basic code is shown below; it generates the structure shown in Figure 3.1.

Structure of a basic app with sidebar

Figure 3.1: Structure of a basic app with sidebar

The following example shows how to use this layout to create a very simple app that demonstrates the Central Limit Theorem. If you run this app yourself, you can see how increasing the number of samples makes a distribution that looks very similar to a normal distribution.

3.4.4 Multi-row

Under the hood, sidebarLayout() is built on top of a flexible multi-row layout, which you can use directly to create more visually complex apps. As usual, you start with fluidPage(). Then you create rows with fluidRow(), and columns with column(). The following template generates the structure shown in Figure 3.2.

The structure underlying a simple multi-row app

Figure 3.2: The structure underlying a simple multi-row app

Note that the first argument to column() is the width, and the width of each row must add up to 12. This gives you substantial flexibility because you can easily create 2-, 3-, or 4-column layouts (more than that starts to get cramped), or use narrow columns to create spacers.

3.4.5 Themes

In Chapter XYZ, we’ll cover the full details of customising the visual appearance of your Shiny app. Creating a complete theme from scratch is a lot of work (but often worth it!), but you can get some easy wins by using the shinythemes package. The following code shows four options:

As you can see, theming your app is quite straightforward: you just need to use the theme argument to fluidPage(). To find out what themes are available, and what they look like, take a look at the Shiny theme selector app at https://shiny.rstudio.com/gallery/shiny-theme-selector.html.

3.4.6 Exercises

  1. Modify the Central Limit Theorem app so that the sidebar is on the right instead of the left.

  2. Browse the themes available in the shinythemes package, pick an attractive theme, and apply it the Central Limit Theorem app.

3.5 Under the hood

In the previous example you might have been surprised to see that I create a Shiny app using a function, theme_demo(). This works because Shiny code is R code, and you can use all of your existing tools for reducing duplication. Remember the rule of three: if you copy and paste code more than three times, you should consider writing a function or using a for loop4.

All input, output, and layout functions return HTML, the descriptive language that underpins every website. You can see that HTML by executing UI functions directly in the console:

Shiny is designed so that, as an R user, you don’t need to learn about the details of HTML. However, if you already know HTML (or want to learn!) you can also work directly with HTML tags to achieve any level of customization you want. And these approaches are by no means exclusive: you can mix high-level functions with low-level HTML as much as you like. We’ll come back to these ideas in Chapter 12, where you’ll learn more about the lower-level features for authoring HTML directly.


  1. All passwordInput() does is hide what the user is typing, so that someone looking over their shoulder can’t read it. It’s up to you to make sure that any passwords are not accidentally exposed, so we don’t recommend using passwords unless you have had some training in secure programming.

  2. Currently Shiny uses Bootstrap 3.3.7, https://getbootstrap.com/docs/3.3/, but the Shiny team is planning to update to 4.0.0, the latest version, in the near future.

  3. Or using lapply() or purrr::map() if you know a little about functional programming.