Interactive reporting with Shiny I

Notes
Modified

May 13, 2026

NoteLearning objectives
  • Introduce Shiny for interactive web applications
  • Review implementation of a basic Shiny app
  • Introduce reactive programming concepts

What is Shiny?

Shiny is a web application framework for R that allows you to build interactive web applications using R code. A Shiny app is a live, running R process: when a reader changes an input (selects a dropdown, moves a slider), the server re-executes R code and sends the updated output back to the browser.

Shiny sits at the most powerful end of the interactivity spectrum:

  1. Interactive charts ({ggiraph}, {plotly}) — client-side only
  2. Scrollytelling — linear, browser-side effects
  3. Dashboards — can use client-side or static server
  4. Interactive web applications (Shiny) — full server-side computation

Because the R process is always running, Shiny apps can do anything R can do: query databases, fit models, process uploaded files, and regenerate any output in response to any input. The tradeoff is that you need a server to host a Shiny app (e.g., shinyapps.io, Posit Connect).

How Shiny works

Diagram showing a user's browser on the left connected via arrows to an R process running locally on the right.

When running locally, the computer serving the app is your own machine.

Diagram showing a user's browser on the left connected via the internet to a web server running an R process on the right.

When deployed, the computer serving the app is a remote web server.

The key insight is that the browser is just a display layer. The computations happen in R on the server, and only the rendered output (HTML, images, tables) is sent to the browser.

Anatomy of a Shiny app

Every Shiny app has exactly two components:

library(shiny)
library(bslib)

ui <- list()

server <- function(input, output, session) {}

shinyApp(ui = ui, server = server)
1
The UI (user interface) defines how the app looks: layouts, input widgets, and output placeholders.
2
The server function contains the reactive logic that generates outputs from inputs.
3
shinyApp() links the UI and server and launches the app.

This structure is the same for apps of any complexity — from a single slider to a multi-page dashboard.

{shiny} and {bslib}

library(shiny)
library(bslib)

library(shiny) + library(bslib) is now the officially recommended way to build Shiny apps. {bslib} provides a modern UI toolkit based on Bootstrap, with cleaner layout functions and better theming support.

A stylistic note: {shiny} functions use camelCase (e.g., plotOutput(), renderPlot()), while {bslib} functions use snake_case (e.g., page_sidebar(), value_box()). This distinction helps identify which package a function comes from.

A first app

The best way to understand Shiny is to build one. The weather dataset contains daily observations from US airports:

d <- read_csv("data/weather.csv")
glimpse(d)
Rows: 23,376
Columns: 17
$ id             <dbl> 72202, 72202, 72202, 72202, 72202, 72202, 72202, 72202, 72202, 72…
$ name           <chr> "Miami", "Miami", "Miami", "Miami", "Miami", "Miami", "Miami", "M…
$ airport_code   <chr> "KMIA", "KMIA", "KMIA", "KMIA", "KMIA", "KMIA", "KMIA", "KMIA", "…
$ country        <chr> "US", "US", "US", "US", "US", "US", "US", "US", "US", "US", "US",…
$ state          <chr> "FL", "FL", "FL", "FL", "FL", "FL", "FL", "FL", "FL", "FL", "FL",…
$ region         <chr> "South", "South", "South", "South", "South", "South", "South", "S…
$ longitude      <dbl> -80.3167, -80.3167, -80.3167, -80.3167, -80.3167, -80.3167, -80.3…
$ latitude       <dbl> 25.7833, 25.7833, 25.7833, 25.7833, 25.7833, 25.7833, 25.7833, 25…
$ date           <date> 2022-01-01, 2022-01-02, 2022-01-03, 2022-01-04, 2022-01-05, 2022…
$ temp_avg       <dbl> 24.2, 24.4, 22.7, 20.4, 22.9, 21.4, 23.6, 23.9, 23.8, 24.0, 21.4,…
$ temp_min       <dbl> 21.1, 21.7, 17.8, 16.7, 19.4, 16.7, 20.6, 22.2, 22.2, 21.1, 18.3,…
$ temp_max       <dbl> 27.8, 28.3, 27.2, 25.6, 27.2, 26.7, 28.3, 26.1, 26.1, 27.8, 25.0,…
$ precip         <dbl> 0.0, 0.0, 0.0, 0.0, 5.3, 0.0, 0.0, 4.3, 4.6, 0.0, 23.9, 0.3, 0.3,…
$ snow           <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ wind_direction <dbl> 130, 164, 261, 27, 142, 330, 5, 58, 87, 53, 5, 58, 328, 319, 75, …
$ wind_speed     <dbl> 12.2, 12.6, 10.8, 10.8, 9.4, 5.0, 7.6, 16.9, 21.6, 6.5, 13.3, 15.…
$ air_press      <dbl> 1018.6, 1018.0, 1018.7, 1019.8, 1015.9, 1016.5, 1019.1, 1024.5, 1…

Here is a minimal app that lets the reader choose an airport and displays a temperature time series:

library(tidyverse)
library(shiny)
library(bslib)

d <- read_csv("data/weather.csv")

ui <- page_sidebar(

  title = "Temperatures at Major Airports",
  sidebar = sidebar(

    radioButtons(
      inputId = "name",
      label = "Select an airport",
      choices = c(
        "Hartsfield-Jackson Atlanta",
        "Raleigh-Durham",
        "Denver",
        "Los Angeles",
        "John F. Kennedy"
      )
    )
  ),
  plotOutput(outputId = "plot")
)

server <- function(input, output, session) {
  output$plot <- renderPlot({

    d |>
      filter(name %in% input$name) |>
      ggplot(mapping = aes(x = date, y = temp_avg)) +
      geom_line()
  })
}

shinyApp(ui = ui, server = server)
1
page_sidebar() from {bslib} creates a two-pane layout with a sidebar on the left and a main body on the right.
2
sidebar() wraps the input widgets that appear in the left panel.
3
inputId = "name" is the ID used to retrieve this input’s current value in the server.
4
plotOutput(outputId = "plot") reserves space in the UI for a plot; it starts empty.
5
renderPlot({...}) in the server generates the plot and assigns it to output$plot, which fills the placeholder in the UI.
6
input$name accesses the current value of the "name" radio button — it is reactive: the plot re-renders automatically whenever the selection changes.

UI layouts

Page layout functions

Shiny uses page layout functions to specify the overall structure of the UI. page_sidebar() is a good default for simple apps: a title, a sidebar (usually holding inputs), and a main body (usually holding outputs).

Other common layouts include:

  • page_fluid() — a flexible multi-row layout
  • page_navbar() — adds a navigation bar with multiple tab pages
  • page_fillable() — content fills the available viewport height

See the full layout gallery at https://shiny.posit.co/r/layouts/.

Shiny vs {bslib} layouts

If you have seen older Shiny code, you may have seen fluidPage() + sidebarLayout() — these are {shiny}’s original layout functions. {bslib} provides modern equivalents (page_sidebar(), page_fluid()) with cleaner syntax. Both work; {bslib} is preferred for new apps.

UI functions generate HTML

Shiny UI functions are R functions that return HTML. Run any of the following in the R console to see the underlying HTML:

actionButton("id", "Click me!")
selectInput("test", "Test", choices = c("A", "B", "C"), selectize = FALSE)
img(src = "shiny.png")
<button id="id" type="button" class="btn btn-default action-button"><span class="action-label">Click me!</span></button>
<div class="form-group shiny-input-container">
  <label class="control-label" id="test-label" for="test">Test</label>
  <div>
    <select class="shiny-input-select form-control" id="test"><option value="A" selected>A</option>
<option value="B">B</option>
<option value="C">C</option></select>
  </div>
</div>
<img src="shiny.png"/>

This means you can generate UI dynamically using standard R code — helpful when building inputs whose choices depend on the data.

Inputs

Input widgets collect user selections and make them available in the server as input$<inputId>. A few key inputs:

radioButtons(

  inputId = "name",
  label = "Select an airport",
  choices = c("Denver", "Los Angeles", "JFK")
)

selectInput(

  inputId = "var",
  label = "Select a variable",
  choices = c("Average temp" = "temp_avg", "Max temp" = "temp_max"),
  selected = "temp_avg"
)

sliderInput(

  inputId = "num",
  label = "Choose a number",
  min = 0,
  max = 100,
  value = 20
)
1
radioButtons() — one selection from a set of options, displayed as a list.
2
selectInput() — a dropdown; set multiple = TRUE to allow multi-select. The vector can have "Display name" = "column_name" form to show a friendly label.
3
sliderInput() — a draggable range slider.

All input widgets follow the same pattern: inputId, label, and widget-specific arguments. The full gallery is at https://shiny.posit.co/r/components/#inputs.

Outputs

Output functions reserve space in the UI for rendered content. They come in matched pairs: one function in the UI, one in the server.

UI function Server function Content
plotOutput() renderPlot({}) A {ggplot2} or base R plot
tableOutput() renderTable({}) An R data frame as an HTML table
uiOutput() renderUI({}) Dynamically generated UI elements
textOutput() renderText({}) A character string

The outputId in the UI must match the name used in output$<name> in the server.

Basic reactivity

The reactive graph

Shiny applications are driven by reactive programming: outputs automatically re-execute whenever the inputs they depend on change. The dependency relationships form a reactive graph.

For the first demo app:

Inputs          Outputs
------          -------
name  --------> plot

When the reader changes the name radio button, Shiny detects that input$name is used inside output$plot’s renderPlot({}) expression, and re-executes just that expression.

Adding a second input

Extending the app to let the reader also choose which weather variable to plot adds a second node to the reactive graph:

d_vars <- c(
  "Average temp" = "temp_avg",
  "Min temp" = "temp_min",
  "Max temp" = "temp_max",
  "Total precip" = "precip",
  "Wind speed" = "wind_speed"
)

ui <- page_sidebar(
  title = "Weather Forecasts",
  sidebar = sidebar(
    radioButtons(
      inputId = "name",
      label = "Select an airport",
      choices = c("Raleigh-Durham", "Denver", "Los Angeles", "JFK")
    ),
    selectInput(
      inputId = "var",
      label = "Select a variable",
      choices = d_vars,
      selected = "temp_avg"
    )
  ),
  plotOutput(outputId = "plot")
)

server <- function(input, output, session) {
  output$plot <- renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
      geom_line() +
      labs(title = str_c(input$name, " - ", input$var))
  })
}

shinyApp(ui = ui, server = server)
1
.data[[input$var]] uses the .data pronoun from {rlang} to reference a data frame column by a variable name stored as a string. This is the idiomatic way to use reactive column names in {tidyverse} pipelines — it avoids the complexity of !!, enquo(), and other tidy evaluation tools.

The reactive graph now has two inputs feeding one output:

Inputs          Outputs
------          -------
name  --------> plot
var   ------/

Both input$name and input$var are detected as dependencies of output$plot, so any change to either triggers a re-render.

Note Application Exercise

AE 21: Building a Shiny weather app

Summary

  • A Shiny app has two components — a UI that defines appearance and an input/output layout, and a server function that contains reactive logic
  • {bslib} provides modern layout functions (page_sidebar(), page_fluid()) that replace the older {shiny} layout functions
  • Input widgets (radioButtons(), selectInput(), sliderInput()) collect reader selections and expose them as input$<id> in the server
  • Output functions come in matched UI/server pairs: plotOutput() / renderPlot({}), tableOutput() / renderTable({}), etc.
  • Shiny automatically detects which outputs depend on which inputs and re-executes only the necessary render functions when an input changes

Acknowledgements

Material derived in part from posit::conf 2025 - Shiny for R Workshop by Colin Rundel (CC BY 4.0) and Advanced Data Visualization.