Introduction to R Shiny

Author

Adam Roberts

Published

June 2, 2026

What is Shiny?

Shiny is an R package that lets you build interactive web applications directly from R without the need for HTML, CSS, or JavaScript (though you can add them if you want). With Shiny, you can create custom websites, tools, and interactive dashboards that anyone can use in a web browser.

This Quarto document is meant to be a brief introduction to Shiny. It draws heavily from Mastering Shiny by Hadley Wickham, and I would recommend reading this (free!) book for a more thorough introduction to R Shiny.

Note: This tutorial assumes basic familiarity with R, including variables, vectors, data frames, and functions. It was developed with assistance from Claude Code.

Why use Shiny?

  • Share your analyses with non-R users through a point-and-click interface
  • Create interactive visualizations that respond to user input
  • Build dashboards for ongoing data monitoring
  • Create tools for social science, like custom survey instruments

Installing Shiny

#if you haven't installed shiny yet, uncomment the line below
#install.packages("shiny")
library(shiny)

The Structure of a Shiny App

Every Shiny app has two main components:

Component Role
UI (User Interface) Defines how your app looks — layout, inputs, outputs
Server Defines how your app works — the R logic that produces outputs

Think of the UI as the front of a restaurant (menus, tables, decor) and the server as the kitchen (where the actual cooking happens). These two components are connected by a function called shinyApp(). Below is an example of an extremely simple, static app that displays a greeting.

library(shiny)

ui <- fluidPage(
  # What the user sees goes here
  "Hello, world"
)

server <- function(input, output) {
  # The logic goes here
}

shinyApp(ui = ui, server = server)

Notice the output about that shows something like:

# Listening on http://127.0.0.1:3827

This is a URL that you can enter into any compatible web browser to open another copy of your app.

Basic workflow of Shiny app development:

  1. Write some code

  2. Start app

  3. Test app (push buttons, move sliders, etc.)

  4. Modify code

  5. Repeat


Your First Shiny App

Now, let’s build a reactive app that greets the user by name.

library(shiny)

ui <- fluidPage(
  titlePanel("My First Shiny App"),

  textInput(
    inputId = "name",
    label   = "Enter your name:",
    value   = ""
  ),

  textOutput("greeting")
)

server <- function(input, output) {
  output$greeting <- renderText({
    paste0("Hello, ", input$name, "!")
  })
}

shinyApp(ui = ui, server = server)

What’s happening here?

  1. titlePanel() adds a title at the top of the page.
  2. textInput() creates a text box. Its inputId = "name" is how we refer to it in the server.
  3. textOutput("greeting") reserves a spot on the page where text will appear.
  4. In the server, output$greeting is assigned the result of renderText(), which reads input$name and pastes a greeting.

Whenever the user types in the box, the greeting updates automatically. This is reactivity.


Understanding Reactivity

Reactivity is the core idea that makes Shiny special. In a regular R script, code runs once from top to bottom. In Shiny, code re-runs automatically whenever an input it depends on changes.

User changes input  →  Server detects change  →  Output re-renders

Reactive rules to remember

  • Inputs (input$...) are read inside render*() or reactive() functions.
  • Outputs (output$...) are assigned using render*() functions.
  • You never call outputs directly. Shiny manages when they update.

Common Input Widgets

Shiny provides many built-in input controls. Here are the most common ones:

ui <- fluidPage(

  # Slider — good for numeric ranges
  sliderInput("num", "Pick a number:", min = 1, max = 100, value = 50),

  # Dropdown — choose one option
  selectInput("fruit", "Favorite fruit:",
              choices = c("Apple", "Banana", "Cherry")),

  # Radio buttons — choose one (all options visible)
  radioButtons("color", "Favorite color:",
               choices = c("Red", "Green", "Blue", "Purple")),

  # Checkbox — TRUE/FALSE toggle
  checkboxInput("agree", "I agree to the terms", value = FALSE),

  # Date picker
  dateInput("date", "Select a date:", value = Sys.Date()),

  # Numeric input box
  numericInput("age", "Your age:", value = 25, min = 0, max = 120)
)

server <- function(input, output) {
  # The logic goes here
}

shinyApp(ui = ui, server = server)

Each widget has an inputId (e.g. “age”) and a label (e.g. “Your age:”).


Common Output Functions

Outputs come in matched pairs: a render*() function in the server, and a *Output() function in the UI.

UI function Server function Produces
textOutput() renderText() Plain text
verbatimTextOutput() renderPrint() Console-style code display
plotOutput() renderPlot() A plot
tableOutput() renderTable() A simple table
dataTableOutput() renderDataTable() An interactive table
uiOutput() renderUI() Dynamic UI elements

Building a Plotting App

Now let’s build an app that lets the user explore the built-in Seatbelts dataset.

library(shiny)

sb <- as.data.frame(Seatbelts)

ui <- fluidPage(
  titlePanel("Explore the Seatbelts Dataset"),

  sidebarLayout(
    sidebarPanel(
      selectInput(
        inputId  = "x_var",
        label    = "X-axis variable:",
        choices  = names(sb),
        selected = "kms"
      ),
      selectInput(
        inputId  = "y_var",
        label    = "Y-axis variable:",
        choices  = names(sb),
        selected = "DriversKilled"
      ),
      sliderInput(
        inputId = "point_size",
        label   = "Point size:",
        min = 1, max = 5, value = 2
      )
    ),

    mainPanel(
      plotOutput("scatter_plot"),
      verbatimTextOutput("correlation")
    )
  )
)

server <- function(input, output) {

  output$scatter_plot <- renderPlot({
    plot(
      x    = sb[[input$x_var]],
      y    = sb[[input$y_var]],
      xlab = input$x_var,
      ylab = input$y_var,
      main = paste(input$y_var, "vs", input$x_var),
      pch  = 19,
      cex  = input$point_size,
      col  = "steelblue"
    )
  })

  output$correlation <- renderPrint({
    r <- cor(sb[[input$x_var]], sb[[input$y_var]])
    cat("Pearson correlation:", round(r, 3))
  })
}

shinyApp(ui = ui, server = server)

Key layout functions used

  • sidebarLayout() — splits the page into a narrow sidebar and a wide main area.
  • sidebarPanel() — holds the controls on the left.
  • mainPanel() — holds the outputs on the right.

Layout Options

Shiny gives you several ways to arrange your app.

fluidPage with columns

ui <- fluidPage(
    fluidRow(
      column(6, div("Left column (width 6)")),
      column(2, div("Middle column (width 2)")),
      column(4, div("Right column (width 4)"))
    )
  )

  server <- function(input, output) {}

  shinyApp(ui = ui, server = server)

Shiny uses a 12-column grid system. Column widths must add up to 12 per row.

tabsetPanel — tabs inside a panel

ui <- fluidPage(
    mainPanel(
      tabsetPanel(
        tabPanel("Plot",    plotOutput("my_plot")),
        tabPanel("Summary", verbatimTextOutput("my_summary")),
        tabPanel("Table",   tableOutput("my_table"))
      )
    )
  )

server <- function(input, output) {
  
}

shinyApp(ui = ui, server = server)

Reactive Expressions

When the same computation is used by multiple outputs, use reactive() to compute it once and share the result. This avoids redundant work and keeps code clean.

library(shiny)

sb <- as.data.frame(Seatbelts)

ui <- fluidPage(
  titlePanel("Reactive Expressions Example"),
  sidebarLayout(
    sidebarPanel(
      radioButtons("law", "Seatbelt law in effect?",
                   choices = c("No" = 0, "Yes" = 1),
                   selected = 0)
    ),
    mainPanel(
      plotOutput("plot"),
      verbatimTextOutput("summary")
    )
  )
)

server <- function(input, output) {

  # Computed once, shared by both outputs below
  filtered_data <- reactive({
    sb[sb$law == input$law, ]
  })

  output$plot <- renderPlot({
    plot(filtered_data()$kms, filtered_data()$DriversKilled,
         xlab = "Distance driven (km)",
         ylab = "Drivers killed",
         main = paste("Law in effect:", ifelse(input$law == 1, "Yes", "No")),
         pch = 19, col = "steelblue")
  })

  output$summary <- renderPrint({
    summary(filtered_data())
  })
}

shinyApp(ui = ui, server = server)

Notice that you call a reactive expression like a function: filtered_data() (with parentheses).


Observers and Side Effects

Sometimes you want to run code in response to an input without producing an output. For example, you may want to print to the console, write a file, or show a notification. Use observe() or observeEvent() for this.

library(shiny)

ui <- fluidPage(
    titlePanel("Name Submitter"),

    textInput(
      inputId = "name",
      label   = "Enter your name:",
      value   = ""
    ),

    actionButton("submit_btn", "Submit"),
)

server <- function(input, output) {

  # Runs whenever input$name changes
  observe({
    cat("User typed:", input$name, "\n")
  })

  # Runs only when the button is clicked
  observeEvent(input$submit_btn, {
    showNotification("Name submitted!", type = "message")
  })
}

shinyApp(ui = ui, server = server)

observeEvent() is preferred when you want to react to a specific trigger (like a button click).


Action Buttons

Action buttons prevent the app from reacting until the user is ready.

ui <- fluidPage(
  numericInput("n", "Number of samples:", value = 100),
  actionButton("go", "Generate!"),
  plotOutput("hist")
)

server <- function(input, output) {

  # Only re-runs when the button is clicked, not when input$n changes
  data <- eventReactive(input$go, {
    rnorm(input$n)
  })

  output$hist <- renderPlot({
    hist(data(), main = "Random Normal Samples", col = "steelblue")
  })
}

shinyApp(ui = ui, server = server)

eventReactive() is like reactive(), but it only updates when a specific event (the button click) occurs.


A Complete Example: Histogram Explorer

Here is a full, self-contained app that brings together everything covered so far.

library(shiny)

ui <- navbarPage(
  title = "Histogram Explorer",

  tabPanel(
    "App",
    sidebarLayout(
      sidebarPanel(
        selectInput("dataset", "Dataset:",
                    choices = c("faithful", "airquality", "mtcars")),
        uiOutput("variable_selector"),
        sliderInput("bins", "Number of bins:", min = 5, max = 50, value = 20),
        selectInput("color", "Bar color:",
                    choices = c("steelblue", "tomato", "seagreen", "goldenrod")),
        checkboxInput("density", "Overlay density curve", value = FALSE),
        actionButton("go", "Update Plot", class = "btn-primary")
      ),
      mainPanel(
        tabsetPanel(
          tabPanel("Plot",    plotOutput("histogram", height = "400px")),
          tabPanel("Summary", verbatimTextOutput("stats")),
          tabPanel("Data",    dataTableOutput("data_table"))
        )
      )
    )
  ),

  tabPanel(
    "About",
    h3("About this app"),
    p("This app demonstrates core Shiny concepts using built-in R datasets."),
    p("Select a dataset and variable, adjust the bins, and click Update Plot.")
  )
)

server <- function(input, output) {

  # Dynamically generate the variable selector based on chosen dataset
  output$variable_selector <- renderUI({
    df <- get(input$dataset)
    num_vars <- names(df)[sapply(df, is.numeric)]
    selectInput("variable", "Variable:", choices = num_vars)
  })

  # Load and validate selected data, only on button click
  selected_data <- eventReactive(input$go, {
    req(input$variable)   # req() stops execution if input$variable is NULL
    df <- get(input$dataset)
    df[[input$variable]]
  }, ignoreNULL = FALSE)

  output$histogram <- renderPlot({
    req(selected_data())
    x <- selected_data()

    hist(x,
         breaks = input$bins,
         col    = input$color,
         border = "white",
         main   = paste("Histogram of", input$variable),
         xlab   = input$variable,
         freq   = !input$density)

    if (input$density) {
      lines(density(x, na.rm = TRUE), col = "black", lwd = 2)
    }
  })

  output$stats <- renderPrint({
    req(selected_data())
    summary(selected_data())
  })

  output$data_table <- renderDataTable({
    req(input$variable)
    df <- get(input$dataset)
    df
  })
}

shinyApp(ui = ui, server = server)

Running and Sharing Your App

Running locally

Save your app as app.R (both UI and server in one file) and run:

shiny::runApp("path/to/your/app")

Or click Run App in RStudio.

Two-file structure (for larger apps)

For bigger projects, split into two files in the same folder:

  • ui.R — contains the ui object
  • server.R — contains the server function

Shiny automatically finds and connects them.

Sharing your app

Option Description
shinyapps.io Free hosting from Posit; deploy with rsconnect::deployApp()
Posit Connect Enterprise hosting for organizations
Shiny Server Self-hosted, open-source option
# Deploy to shinyapps.io (one-time setup required)
install.packages("rsconnect")
rsconnect::deployApp("path/to/your/app")

Tips and Next Steps

Helpful functions to know

  • req() — stops execution gracefully if an input is NULL or empty
  • validate() / need() — show user-friendly error messages
  • withProgress() — show a progress bar for slow operations
  • isolate() — read an input without creating a reactive dependency

Packages that extend Shiny

Package What it adds
shinythemes Bootstrap themes (cosmo, flatly, darkly, etc.)
bslib Modern Bootstrap 5 theming and components
shinyWidgets Extra input widgets (switches, pickers, etc.)
DT Feature-rich interactive data tables
plotly Interactive, zoomable plots
leaflet Interactive maps
shinydashboard Dashboard-style layout

Where to learn more

  • Official documentation and gallery: shiny.posit.co
  • Mastering Shiny (free online book): mastering-shiny.org
  • RStudio Community forum: community.rstudio.com

Summary

Concept Key function(s)
App structure fluidPage(), shinyApp()
Inputs sliderInput(), selectInput(), textInput(), …
Outputs (UI) plotOutput(), textOutput(), tableOutput(), …
Outputs (server) renderPlot(), renderText(), renderTable(), …
Reactive computation reactive()
Event-driven computation eventReactive(), observeEvent()
Side effects observe(), observeEvent()
Safety checks req(), validate()

You now have everything you need to build your first Shiny app. Start small, iterate often, and consult the Shiny gallery for inspiration!