#if you haven't installed shiny yet, uncomment the line below
#install.packages("shiny")
library(shiny)Introduction to R Shiny
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
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:
Write some code
Start app
Test app (push buttons, move sliders, etc.)
Modify code
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?
titlePanel()adds a title at the top of the page.textInput()creates a text box. ItsinputId = "name"is how we refer to it in the server.textOutput("greeting")reserves a spot on the page where text will appear.- In the server,
output$greetingis assigned the result ofrenderText(), which readsinput$nameand 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 insiderender*()orreactive()functions. - Outputs (
output$...) are assigned usingrender*()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).
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 theuiobjectserver.R— contains theserverfunction
Shiny automatically finds and connects them.
Tips and Next Steps
Helpful functions to know
req()— stops execution gracefully if an input is NULL or emptyvalidate()/need()— show user-friendly error messageswithProgress()— show a progress bar for slow operationsisolate()— 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!