RaukR 2025 • R Beyond the Basics
Roy Francis
17-Jun-2025
Shiny use cases, App structure, Code execution,
UI, Widgets, Dynamic UI,
Reactivity, Reactive dynamics,
Error validation, Observers, Reactive values,
Updating widgets, Action buttons, Download, Modules, Debugging,
Theming, Deploy,
Interactive documents, Extensions
What is Shiny?
R framework for building interactive web applications
One file format
shiny::runApp("path/to/folder")
R -e "shiny::runApp('~/shinyapp')"
User Interface (UI)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: ui
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
titlePanel("Title Panel"),
sidebarLayout(
sidebarPanel(
helpText("Sidebar Panel")
),
mainPanel(
"hello"
)
)
),
server=function(input,output) {})
Hypertext Markup Language (HTML)
<div class="col-sm-4">
<form class="well" role="complementary">
<span class="help-block">Sidebar Panel</span>
</form>
</div>
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: ui-2
#| standalone: true
#| components: [editor, viewer]
#| viewerHeight: 300
shinyApp(
ui = fluidPage(
titlePanel("Title Panel"),
sidebarLayout(
sidebarPanel(helpText("Sidebar Panel")),
mainPanel(tabsetPanel(
tabPanel(
"tab1",
fluidRow(
column(6, helpText("Col1")),
column(
6,
helpText("Col2"),
fluidRow(
column(4, style = "background-color:#b0c6fb", helpText("Col1")),
column(4, style = "background-color:#ffa153", helpText("Col2")),
column(4, style = "background-color:#b1f6c6", helpText("Col3"))
)
)
)
),
tabPanel(
"tab2",
inputPanel(helpText("Input Panel"))
),
tabPanel(
"tab3",
wellPanel(helpText("Well Panel"))
)
))
)
),
server = function(input, output) {}
)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: inputs
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
fluidRow(
column(
4,
selectInput("select-input",
label = "selectInput",
choices = c("A", "B", "C")
),
)
)
),
server = function(input, output) {
}
)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: input-widgets
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
fluidRow(
column(4,
fileInput("file-input","fileInput:"),
selectInput("select-input",label="selectInput",choices=c("A","B","C")),
sliderInput("slider-input",label="sliderInput",value=5,min=1,max=10),
numericInput("numeric-input",label="numericInput",value=5,min=1,max=10),
textInput("text-input",label="textInput"),
textAreaInput("text-area-input",label="textAreaInput"),
dateInput("date-input",label="dateInput"),
dateRangeInput("date-range-input",label="dateRangeInput"),
radioButtons("radio-button",label="radioButtons",choices=c("A","B","C"),inline=T),
checkboxInput("checkbox","checkboxInput",value=FALSE),
actionButton("action-button","Action"),
hr(),
submitButton()
)
)
),
server=function(input,output) {
})
Function | Description |
---|---|
checkboxInput() |
Checkbox |
checkboxGroupInput() |
Checkbox group |
radioButtons() |
Radio buttons |
dateInput() |
Single date |
dateRangeInput() |
Date range |
fileInput() |
Upload file |
numericInput() |
Input number |
sliderInput() |
Input number |
Function | Description |
---|---|
textInput() |
Single line text input |
textAreaInput() |
Multi-line text input |
passwordInput() |
Password input |
selectInput() |
Dropdown selection |
actionButton() |
Action button |
submitButton() |
Submit button |
tabsetPanel() |
Tabset panel |
navbarPage() |
Page with navbar |
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: output
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
textInput("text_input",label="textInput",value="hello world"),
textOutput("text_output")
),
server=function(input, output) {
output$text_output <- renderText({input$text_input})
})
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: output-widgets
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(fluidRow(column(5,
textInput("text_input",label="textInput",value="<h3 style='color:red'>Red text</h3>"),
hr(),
htmlOutput("html_output"),
textOutput("text_output"),
verbatimTextOutput("verbatim_text_output"),
tableOutput("table_output"),
plotOutput("plot_output",width="300px",height="300px")
))),
server=function(input, output) {
output$html_output <- renderText({input$text_input})
output$text_output <- renderText({input$text_input})
output$verbatim_text_output <- renderText({input$text_input})
output$table_output <- renderTable({iris[1:3,1:3]})
output$plot_output <- renderPlot({
plot(iris[,1],iris[,2])
})
})
Output | Renderer | Description |
---|---|---|
textOutput() |
renderText() /renderPrint() |
Standard text |
verbatimTextOutput() |
renderText() /renderPrint() |
Monospaced text |
htmlOutput() |
renderText() /renderPrint() |
HTML text output |
plotOutput() |
renderPlot() |
Create and display image |
imageOutput() |
renderImage() |
Display existing image |
tableOutput() |
renderTable() |
Table output |
uiOutput() |
renderUI() |
HTML components |
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: complete-app
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
h3("Temperature Converter"),
numericInput("celsius", "Degrees Celsius:", value = 0),
textOutput("fahrenheit")
),
server = function(input, output) {
output$fahrenheit <- renderText({
paste(input$celsius, "°C is ", (input$celsius * 9/5) + 32, " °F")
})
})
uiOutput()
/renderUI()
shinyApp(
ui=fluidPage(
selectInput("type",label="Select input type", choices=c("Text","Number")),
uiOutput("ui"),
textOutput("text_output")
),
server=function(input, output) {
output$ui <- renderUI({
if(input$type=="Text") {
textInput("input_text","Enter text")
}else{
sliderInput("input_number", "Select number", value=5, min=1, max=10)
}
})
output$text_output <- renderText({
if(input$type=="Text") {
input$input_text
}else{
input$input_number
}
})
})
conditionalPanel()
, insertUI()
and removeUI()
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: render-ui-1
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
selectInput("type", label="Select input type", choices=c("Text","Number")),
uiOutput("ui"),
textOutput("text_output")
),
server=function(input, output) {
output$ui <- renderUI({
if(input$type=="Text") {
textInput("input_text", "Enter text", value="hello")
}else{
sliderInput("input_number", "Select number", value=5, min=1, max=10)
}
})
output$text_output <- renderText({
if(input$type=="Text") {
input$input_text
}else{
input$input_number
}
})
})
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: render-ui-2
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
selectInput("data",label="Select data",
choices=c("mtcars","faithful","iris")),
tableOutput("table"),
uiOutput("ui")
),
server=function(input, output) {
data <- reactive({ get(input$data, 'package:datasets') })
output$ui <- renderUI({
if(input$data=="iris") plotOutput("plot",width="400px")
})
output$plot <- renderPlot({hist(data()[, 1])})
output$table <- renderTable({head(data())})
})
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: render-ui-3
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
sliderInput("persons",label="Select number of persons", value=1, min=1,max=4),
uiOutput("ui")
),
server=function(input, output) {
output$ui <- renderUI({
lapply(1:input$persons, function(i) {
div(
textInput(paste0("name",i),"Enter name:",paste0("Person ",i)),
textInput(paste0("tel",i),"Enter phone number:",value = "0773921562"),
hr()
)
})
})
})
Reactive programming
Functions with reactive context
reactive()
: Defines an expressionreactiveVal()
: Defines single valuereactiveValues()
: Defines a list of valuesRegular function fn <- function()
: Runs wherever it is is used
Reactive function fn <- reactive()
: Runs only when input changes
Reactive values cannot be accessed outside a reactive context
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: reactive-context
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
numericInput("num", "Enter number",
value = 1),
textOutput("result")
),
server = function(input, output) {
val = input$num^2
output$result <- renderText({
paste("Squared number is:", val)
})
}
)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: reactive-dynamics-1
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
numericInput("num_input",label="Observations",value=50),
textOutput("text_output")),
server=function(input,output) {
output$text_output <- renderText({
mean(rnorm(input$num_input))
})
})
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: reactive-dynamics-2
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
numericInput("num_input",label="Observations",value=50),
textOutput("text_output1"), textOutput("text_output2")),
server=function(input,output) {
output$text_output1 <- renderText({
mean(rnorm(input$num_input))
})
output$text_output2 <- renderText({
mean(rnorm(input$num_input))
})
})
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: reactive-dynamics-3
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
numericInput("num_input",label="Observations",value=50),
textOutput("text_output1"), textOutput("text_output2")),
server=function(input,output) {
rand <- function(x) rnorm(x)
output$text_output1 <- renderText({
mean(rand(input$num_input))
})
output$text_output2 <- renderText({
mean(rand(input$num_input))
})
})
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: reactive-dynamics-4
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui=fluidPage(
numericInput("num_input",label="Observations",value=50),
textOutput("text_output1"), textOutput("text_output2")
),
server=function(input,output) {
rand <- reactive({ rnorm(input$num_input) })
output$text_output1 <- renderText({
mean(rand())
})
output$text_output2 <- renderText({
mean(rand())
})
})
validate()
can be used to check inputvalidate()
using need()
shinyApp(
ui=fluidPage(
selectInput("data_input",label="Select data",
choices=c("","unknown","mtcars","faithful","iris")),
tableOutput("table_output")
),
server=function(input, output) {
getdata <- reactive({
validate(need(try(input$data_input),"Please select a data set"))
get(input$data_input,'package:datasets')
})
output$table_output <- renderTable({head(getdata())})
})
validate()
using custom functionvalfn <- function(x) if(is.null(x) | is.na(x) | x=="") return("Input data is incorrect.")
shinyApp(
ui=fluidPage(
selectInput("data_input",label="Select data",
choices=c("","unknown","mtcars","faithful","iris")),
tableOutput("table_output")
),
server=function(input,output) {
getdata <- reactive({
validate(valfn(try(input$data_input)))
get(input$data_input,'package:datasets')
})
output$table_output <- renderTable({head(getdata())})
})
shiny::req()
checks input variable and silently stops executionReactive functions that automatically re-executes whenever its dependencies change. Usually used for side-effects rather than returning a value.
observe()
Doesn’t return a value. Constantly runs as long as the app is alive.
observeEvent()
Similar to observe()
, but only re-executes on specific event (like a button click). Doesn’t return a value.
eventReactive()
Similar to observeEvent()
, but returns a value.
bindEvent()
Bind an action to a specific event. Similar to observeEvent()
, but allows for the event to be determined dynamically. Links an input (like a button) to an output and ensures that reactive expressions are only updated when bound event is triggered.
observe({})
monitors for a conditional change#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: updating-widgets
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
selectInput("data", "Choose a dataset:", choices = c("mtcars", "iris", "faithful")),
selectInput("variable", "Choose a variable:", choices = names(mtcars))
),
server = function(input, output, session) {
dataset <- reactive({
switch(input$data,
"mtcars" = mtcars,
"iris" = iris,
"faithful" = faithful)
})
observe({
updateSelectInput(session,"variable",choices=names(dataset()))
})
}
)
Input function | Update function |
---|---|
checkboxInput() |
updateCheckboxInput() |
checkboxGroupInput() |
updateCheckboxGroupInput() |
radioButtons() |
updateRadioButtons() |
dateInput() |
updateDateInput() |
dateRangeInput() |
updateDateRangeInput() |
fileInput() |
|
numericInput() |
updateNumericInput() |
sliderInput() |
updateSliderInput() |
textInput() |
updateTextInput() |
textAreaInput() |
updateTextAreaInput() |
passwordInput() |
|
selectInput() |
updateSelectInput() |
actionButton() |
|
submitButton() |
|
tabsetPanel() |
updateTabsetPanel() |
navbarPage() |
updateNavbarPage() |
reactVal()
reactValues()
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: reactive-values
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
actionButton("add","Add"),
actionButton("subtract","Subtract"),
textOutput("counter")
),
server = function(input, output) {
reactive_values <- reactiveValues(counter = 0)
observeEvent(input$add, {
reactive_values$counter <- reactive_values$counter + 1
})
observeEvent(input$subtract, {
reactive_values$counter <- reactive_values$counter - 1
})
output$counter <- renderText({
reactive_values$counter
})
}
)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: buttons
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
h3("Temperature Converter"),
numericInput("celsius", "Degrees Celsius:", value = 0),
actionButton("btn_go", "Go!"),
textOutput("fahrenheit")
),
server = function(input, output) {
output$fahrenheit <- renderText({
paste(input$celsius, "°C is ", (input$celsius * 9/5) + 32, " °F")
}) |>
bindEvent(input$btn_go)
# evr <- eventReactive(input$btn_go, {
# paste(input$celsius, "°C is ", (input$celsius * 9/5) + 32, " °F")
# })
# output$fahrenheit <- renderText({
# evr()
# })
})
downloadHandler()
functionshinyApp(
ui=fluidPage(
selectInput("data_input",label="Select data", choices=c("mtcars","faithful","iris")),
textOutput("text_output"),
downloadButton("button_download","Download")),
server=function(input, output) {
getdata <- reactive({ get(input$data_input, 'package:datasets') })
output$text_output <- renderText(paste0("Selected dataset: ",input$data_input))
output$button_download <- downloadHandler(
filename = function() {
paste0(input$data_input,".csv")
},
content = function(file) {
write.csv(getdata(),file,row.names=FALSE,quote=F)
})
})
shinyApp(
ui=fluidPage(
selectInput("data_input",label="Select data",
choices=c("mtcars","faithful","iris")),
textOutput("text_output"),
plotOutput("plot_output",width="400px"),
downloadButton("button_download", "Download")
),
server=function(input, output) {
getdata <- reactive({ get(input$data_input, 'package:datasets') })
output$text_output <- renderText(paste0("Selected dataset: ",input$data_input))
output$plot_output <- renderPlot({hist(getdata()[, 1])})
output$button_download <- downloadHandler(
filename = function() {
paste0(input$data_input,".png")
},
content = function(file) {
png(file)
hist(getdata()[, 1])
dev.off()
})
})
NS()
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: modules
#| standalone: true
#| components: [editor, viewer]
counter_ui <- function(id) {
ns <- NS(id)
div(
actionButton(ns("btn"), label = "Counter"),
textOutput(ns("txt"))
)
}
counter_server <- function(id) {
moduleServer(id, function(input, output, session) {
count <- reactiveVal(0)
output$txt <- renderText({
count(count() + 1)
paste0("Counter ", id, ":", count())
}) |> bindEvent(input$btn)
})
}
shinyApp(
ui = fluidPage(
counter_ui(id = "1"),
counter_ui(id = "2")
),
server = function(input, output, session) {
counter_server("1")
counter_server("2")
}
)
print()
statements
Interrupt execution and inspect environment browser()
Visualize relationships using reactlog
Assess compute and RAM usage using profvis
Simple profiling using shiny.tictoc
App launch
New user
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| label: shinythemes
#| standalone: true
#| viewerHeight: 650
webr::install("shinythemes")
shinyApp(
ui = fluidPage(
shinythemes::themeSelector(),
sidebarPanel(
textInput("txt", "Text input:", "text here"),
sliderInput("slider", "Slider input:", 1, 100, 30),
actionButton("action", "Button"),
actionButton("action2", "Button2", class = "btn-primary")
),
mainPanel(
tabsetPanel(
tabPanel("Tab 1"),
tabPanel("Tab 2")
)
)
),
server = function(input, output) {}
)
www/
---
title: "Interactive scatterplot"
format: html
server: shiny
---
```{r}
library(shiny)
library(ggplot2)
selectInput("x_var", "X-axis Variable:", choices=names(mtcars), selected = "hp"),
selectInput("y_var", "Y-axis Variable:", choices=names(mtcars), selected = "mpg"),
plotOutput("plot")
```
```{r}
#| context: server
output$plot <- renderPlot({
ggplot(mtcars, aes_string(x = input$x_var, y = input$y_var)) +
geom_point() +
labs(title = "Interactive mtcars scatterplot",
x = input$x_var,
y = input$y_var)
})
```
<iframe>
<iframe src="https://user.shinyapps.io/app"></iframe>
Documentation
Books
Conferences
Blogs & Podcasts