Inputs and Outputs

Last updated on 2026-10-27 | Edit this page

Overview

Questions

  • How can users interact with a Shiny app?
  • How do user inputs control what is displayed in the app?

Objectives

  • Load data for use in a Shiny app
  • Add a user input to control app behaviour
  • Use input values in the server to update outputs

So far, we have built the structure of our app and created a user interface, but the app is still static. In this episode, we add user inputs and use them in the server to control what is displayed. This is where the app becomes interactive.

Before we can respond to user input, we need to prepare the data the app will use.

Prepare Data


This lesson uses data from an Employment Projection Model, which provides employment forecasts for South East Queensland. The dataset includes forecast information for 2041, such as occupations and industries associated with residents in each region.

Check that the csv file used in this workshop is located in your project’s data folder. Then add the following lines to the setup section of your app to load the data.

We also load the tidyverse package, which we will use for reading and manipulating the data.

R

# 1. Setup
library(shiny)
library(tidyverse)

bcc <- read_csv("data/bcc_occupation_industry.csv")
str(bcc)

OUTPUT

spc_tbl_ [399 × 6] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ region               : chr [1:399] "Brisbane - East" "Brisbane - East" "Brisbane - East" "Brisbane - East" ...
 $ suburb               : chr [1:399] "Belmont - Gumdale" "Belmont - Gumdale" "Belmont - Gumdale" "Brisbane Port - Lytton" ...
 $ occupation           : chr [1:399] "Specialist Managers" "Health Professionals" "Business, Human Resource and Marketing Professionals" "Chief Executives, General Managers and Legislators" ...
 $ occupation_count_2041: num [1:399] 374 306 232 0 0 0 398 201 173 569 ...
 $ industry             : chr [1:399] "Professional, Scientific and Technical Services (Except Computer System Design and Related Services)" "Hospitals" "Tertiary Education" "Agriculture" ...
 $ industry_count_2041  : num [1:399] 341 308 223 0 0 0 395 221 213 595 ...
 - attr(*, "spec")=
  .. cols(
  ..   region = col_character(),
  ..   suburb = col_character(),
  ..   occupation = col_character(),
  ..   occupation_count_2041 = col_double(),
  ..   industry = col_character(),
  ..   industry_count_2041 = col_double()
  .. )
 - attr(*, "problems")=<pointer: 0x560e49a6cf60> 
Challenge

Challenge

Explore the data

Take a minute to inspect the dataset and identify variables that a user might reasonably control using an input object (for example, via a dropdown menu).

Examples of variables that could be controlled by a user include region, suburb, and occupation.

Variables that represent derived values or rankings (such as counts or ranks) are less suitable as direct user inputs at this stage.

Adding Inputs to the User Interface


Now that we have data, we need to give our user the tools to explore it. Shiny provides a wide range of functions for creating Inputs. These functions follow a consistent naming pattern, using camelCase and ending in Input - such as selectInput() and sliderInput().

The function then takes an inputID argument - a string which will be used to refer to the input later on in the server. Then a label argument, for what the user will see as the text describing the input. Finally, a wide variety of input specific arguments.

For our app, we want to add a selectInput() that allows the user to choose a region.

Looking at ?selectInput we see we need the following arguments:

  • inputId - a name for our input
  • label - what the user sees
  • choices - some choices select from. It’s good practice to use the data for these values, e.g. with the unique() function, to ensure you do not enter a value that is not found in the data.

Let’s insert an input object into our sidebarPanel():

R

# 2. Define a User Interface
ui <- fluidPage(
  titlePanel("This is the title panel"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
                inputId = "region_select",
                label = "Choose a region", 
                choices = unique(bcc$region)
                )
      ),
    mainPanel("put outputs here")
  )
)

Save and run your app, and see that a dropdown menu with the different regions appears. Change the selected region. Notice that although the input value changes, the app output does not yet respond. This will change in the next section.

Callout

Input Widgets

This is just the start of what is possible with input widgets. Shiny provides many built‑in inputs, and additional packages extend these options further.

Example of Shiny inputs

Adding Outputs to the User Interface


Next we need to tell Shiny where outputs should appear in the user interface. Much like the *Input() functions, Shiny provides a set of *Output() functions that define placeholders in the UI where results will be displayed.

Function Output Type
plotOutput() plot, ggplot
tableOutput() table or data frame
textOutput() text
htmlOutput() HTML code

Like inputs, these functions have a first argument - in this case outputId which is a character string, and will be used elsewhere in the app. Other arguments to these functions vary by output, but can be used to do things like specify output window size, scaling, and more.

Let’s add a tableOutput() to our app that will display the top occupations of the selected regions. This will go in our mainPanel():

R

# 2. Define a User Interface
ui <- fluidPage(
  titlePanel("This is the title panel"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
                inputId = "region_select",
                label = "Choose a region", 
                choices = unique(bcc$region)
                )
      ),
    mainPanel(
      tableOutput(outputId = "occupation_table")      # this is new!
    )
  )
)

Render your app to make sure it works.

Notice that the table does not show up! This is expected: we have defined a placeholder in the UI, but we have not yet told the server how to produce the output.

To make this output appear, we now need to add server logic that tells Shiny how to create the table output based on the selected region.

Using input values in the server to produce outputs


It is now time to look more closely at the server. Recall that the server code looks like this:

R

# 3. define a server
server <- function(input, output) {}

At the moment, this is just an empty function. It is the code we place inside this function that gives the app its behaviour.

The server function takes two arguments: input and output. These are lists created by Shiny.

  • The input list stores values coming from the input widgets defined in the UI.
  • The output list stores results generated by the server that will be displayed in the UI.

The names used in the UI connect these lists together. For example, the value selected using our selectInput() can be accessed as input$region_select, and the output we defined using tableOutput() will be referred to as output$occupation_table.

Generating outputs in the server

To generate output in a Shiny app, we add elements to the output list inside the server function. Each output is created using a render*() function that matches the corresponding *Output() function defined in the UI.

A useful pattern to remember is that:

  • *Output() functions define where output appears in the UI
  • render*() functions define how that output is created in the server

Shiny provides a family of render*() functions, such as renderPlot(), renderTable(), and others.

The code inside a render*() function is re‑run automatically whenever its dependencies change. This allows the app to respond dynamically to user interaction.

Example of renderPlot function identifying the function that identifies what type of object to build and the code to build it Note that the code inside render*() functions is enclosed in curly braces {}. These braces define a block of code that Shiny runs to generate the output. You can think of them as grouping multiple lines of code together and passing them to the rendering function as a single unit.

We now modify the server to generate the output defined in the user interface.

To create our table, we use functions from the tidyverse to filter the data based on the region selected by the user, using the value stored in input$region_select, and then select the relevant columns to display in the output table.

Specifically, we want to:

  • filter() the data to the selected region.
  • group_by() to group rows by occupation.
  • summarise() to combine duplicate occupations by summing occupation_count_2041 so we return one row per occupation and drop the grouping.
  • arrange() the data in descending order.

R

# 3. Define a server
server <- function(input, output) {
  
    output$occupation_table <- renderTable({
      bcc %>%
        filter(region == input$region_select) %>%
        group_by(occupation) %>%
        summarise(
          occupation_count_2041 = sum(occupation_count_2041, na.rm = TRUE),
          .groups = "drop"
        ) %>%
        arrange(desc(occupation_count_2041))
    })
}

Save your script and run the app again. This time, the occupation_table appears in the main panel and updates when a different region is selected.

Discussion

How did it go?

And there you have it - a basic Shiny app! Take a minute to interact with the app by selecting different regions.

  1. What do you learn from exploring the data this way?

  2. How might you change or extend this app to make it more useful?

Challenge

Modify the output table

Update the server code so that the table displays only the occupation names, and no longer includes the occupation count column.

One possible solution is shown below.

Here, the select() function is used to drop the occupation_count_2041 column.

R

# 3. Define a server
server <- function(input, output) {
  
  output$occupation_table <- renderTable({
    bcc %>%
      filter(region == input$region_select) %>%
      group_by(occupation) %>%
      summarise(
        occupation_count_2041 = sum(occupation_count_2041, na.rm = TRUE),
        .groups = "drop"
      ) %>%
      arrange(desc(occupation_count_2041)) %>%
      select(occupation)                        # this is new!
  })
}

In this episode, we built a working Shiny app by connecting user inputs to server logic and producing outputs. We implemented this interactivity by placing all of our data processing directly inside the render*() functions.

This approach works well for simple workflows, where each output depends on a small amount of logic. However, as apps grow and multiple outputs begin to share the same calculations, this pattern can become repetitive and harder to maintain.

Shiny provides reactive expressions to help separate and reuse server logic more effectively. In the next episode, we introduce reactive expressions and show how they can be used to organise more complex apps.

Key Points
  • Shiny apps become interactive by connecting user inputs to server logic that produces outputs.
  • Inputs are defined in the UI using *Input() functions and accessed in the server through the input list.
  • Outputs have placeholders in the UI created with *Output() functions and are generated in the server using matching render*() functions.
  • Code inside a render*() function is re‑run automatically when the input values it depends on change.
  • Server logic controls how user input is translated into displayed output.