Lecture 23
Cornell University
INFO 3312/5312 - Spring 2026
April 23, 2026
ae-23Instructions
ae-23 (repo name will be suffixed with your GitHub name).renv::restore() to install the required packages, open the Quarto document in the repo, and follow along and complete the exercises.Much of the interface provided by Shiny is based on the HTML elements, styling, and Javascript provided by the Bootstrap library.
Knowing the specifics of HTML (and Bootstrap specifically) are not needed for working with Shiny - but understanding some of its conventions goes a long way to helping you customize the elements of your app (via custom CSS and other tools).
This is not the only place that Bootstrap shows up in the R ecosystem - Quarto HTML documents use Bootstrap for styling as well.
One of the other features of the {bslib} package is that it also provides a number of modern UI components that you can use to enhance the look and feel of your Shiny apps.
Some of these components include:
These components all come from Bootstrap and are designed to be modular and flexible so that you can use them in a variety of ways.
Cards are a UI element that you will recognize from many modern websites. They are rectangular containers with borders and padding that are used to group related information. When utilized properly to group elements they help users better digest, engage, and navigate through content
Cards can be styled using the class argument, this is used to apply Bootstrap classes to the card and its components.
Cards are also super flexible and can contain multiple card_body() elements. This can be useful for creating complex layouts.
Value boxes are the other major UI component provided by {bslib}. They are a simple way to display a value and a label in a styled box. They are often used to display key metrics in a dashboard.
demos/demo-07.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Data",
sidebar = sidebar(
selectInput(
"region",
"Select a region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput(
"name",
"Select an airport",
choices = c()
),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
card(
card_header(
textOutput("title")
),
card_body(
plotOutput("plot")
)
)
)
server <- function(input, output, session) {
observe({
updateSelectInput(
session,
"name",
choices = d |>
distinct(region, name) |>
filter(region == input$region) |>
pull(name)
)
})
output$title <- renderText({
names(d_vars)[d_vars == input$var]
})
d_city <- reactive({
req(input$name)
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
theme_minimal()
})
}
shinyApp(ui = ui, server = server)Previously we had included a table that showed minimum and maximum temperatures - lets try presenting these using value boxes instead.
Before we get to the code lets think a little bit about how we might do this:
Any one see a potential issue with this?
Each value box shows a dynamic value that is calculated from the data - so we need a textOutput() and corresponding renderText() for each value box, and will need even more if we want to change the color or icon based on the value.
uiOutput() and renderUI()To handle situations like this Shiny provides the ability to dynamically generate UI elements entirely within the server() function.
For our case we can create all of the value boxes in a single renderUI() call making our code simpler and more maintainable.
Additionally, since renderUI() is a reactive context we can perform all of our calculations in the same place at the same time.
demos/demo-08.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Data",
sidebar = sidebar(
selectInput(
"region",
"Select a region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput(
"name",
"Select an airport",
choices = c()
),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
card(
card_header(
textOutput("title")
),
card_body(
plotOutput("plot")
)
),
uiOutput("valueboxes")
)
server <- function(input, output, session) {
observe({
updateSelectInput(
session,
"name",
choices = d |>
distinct(region, name) |>
filter(region == input$region) |>
pull(name)
)
})
output$valueboxes <- renderUI({
clean <- function(x) {
round(x, 1) |> paste("°C")
}
layout_columns(
value_box(
title = "Average Temp",
value = mean(d_city()$temp_avg, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-half"),
theme = "success"
),
value_box(
title = "Minimum Temp",
value = min(d_city()$temp_min, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-snow"),
theme = "primary"
),
value_box(
title = "Maximum Temp",
value = max(d_city()$temp_max, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-sun"),
theme = "danger"
)
)
})
output$title <- renderText({
names(d_vars)[d_vars == input$var]
})
d_city <- reactive({
req(input$name)
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
theme_minimal()
})
}
shinyApp(ui = ui, server = server) demos/demo-09.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Data",
sidebar = sidebar(
selectInput(
"region",
"Select a region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput(
"name",
"Select an airport",
choices = c()
),
),
card(
card_header(
textOutput("title"),
popover(
bsicons::bs_icon("gear", title = "Settings"),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
class = "d-flex justify-content-between align-items-center"
),
card_body(
plotOutput("plot")
),
full_screen = TRUE
),
uiOutput("valueboxes")
)
server <- function(input, output, session) {
observe({
updateSelectInput(
session,
"name",
choices = d |>
distinct(region, name) |>
filter(region == input$region) |>
pull(name)
)
})
output$valueboxes <- renderUI({
clean <- function(x) {
round(x, 1) |> paste("°C")
}
layout_columns(
value_box(
title = "Average Temp",
value = mean(d_city()$temp_avg, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-half"),
theme = "success"
),
value_box(
title = "Minimum Temp",
value = min(d_city()$temp_min, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-snow"),
theme = "primary"
),
value_box(
title = "Maximum Temp",
value = max(d_city()$temp_max, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-sun"),
theme = "danger"
)
)
})
output$title <- renderText({
names(d_vars)[d_vars == input$var]
})
d_city <- reactive({
req(input$name)
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
theme_minimal()
})
}
shinyApp(ui = ui, server = server)Due to the ubiquity of Bootstrap, a large amount of community effort has already gone into developing custom themes.
bs_theme()Provides a high level interface to adjusting the theme for an entire Shiny app, and is passed to the theme argument of of the page function of our UI (e.g. page_sidebar(), fluidPage(), etc.).
bs_theme() allows allows us to construct a theme object by specifying:
a bootstrap version via version argument
a bootswatch theme via preset or bootswatch arguments
basic color palette values (bg, fg, primary, secondary, etc.)
fonts (base_font, code_font, heading_font, font_scale)
and more …
demos/demo-10.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
theme = bs_theme(),
title = "Weather Data",
sidebar = sidebar(
selectInput(
"region",
"Select a region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput(
"name",
"Select an airport",
choices = c()
),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
card(
card_header(
textOutput("title"),
),
card_body(
plotOutput("plot")
)
),
uiOutput("valueboxes")
)
server <- function(input, output, session) {
bslib::bs_themer()
observe({
updateSelectInput(
session,
"name",
choices = d |>
distinct(region, name) |>
filter(region == input$region) |>
pull(name)
)
})
output$valueboxes <- renderUI({
clean <- function(x) {
round(x, 1) |> paste("°C")
}
layout_columns(
value_box(
title = "Average Temp",
value = mean(d_city()$temp_avg, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-half"),
theme = "success"
),
value_box(
title = "Minimum Temp",
value = min(d_city()$temp_min, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-snow"),
theme = "primary"
),
value_box(
title = "Maximum Temp",
value = max(d_city()$temp_max, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-sun"),
theme = "danger"
)
)
})
output$title <- renderText({
names(d_vars)[d_vars == input$var]
})
d_city <- reactive({
req(input$name)
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
theme_minimal()
})
}
shinyApp(ui = ui, server = server)Simplified theming of {ggplot2}, {lattice}, and {base} R graphics. In addition to providing a centralized approach to styling R graphics, {thematic} also enables automatic styling of R plots in Shiny, R Markdown, and RStudio.
In the case of our Shiny app, to get dynamic theming of our plot as well as our UI all we need to do is to include a call to thematic_shiny() before the app is loaded.

Learn more at https://rstudio.github.io/thematic/
demos/demo-11.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
theme = bs_theme(),
title = "Weather Data",
sidebar = sidebar(
selectInput(
"region",
"Select a region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput(
"name",
"Select an airport",
choices = c()
),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
card(
card_header(
textOutput("title")
),
card_body(
plotOutput("plot")
)
),
uiOutput("valueboxes")
)
server <- function(input, output, session) {
bslib::bs_themer()
observe({
updateSelectInput(
session,
"name",
choices = d |>
distinct(region, name) |>
filter(region == input$region) |>
pull(name)
)
})
output$valueboxes <- renderUI({
clean <- function(x) {
round(x, 1) |> paste("°C")
}
layout_columns(
value_box(
title = "Average Temp",
value = mean(d_city()$temp_avg, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-half"),
theme = "success"
),
value_box(
title = "Minimum Temp",
value = min(d_city()$temp_min, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-snow"),
theme = "primary"
),
value_box(
title = "Maximum Temp",
value = max(d_city()$temp_max, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-sun"),
theme = "danger"
)
)
})
output$title <- renderText({
names(d_vars)[d_vars == input$var]
})
d_city <- reactive({
req(input$name)
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line()
})
}
thematic::thematic_shiny()
shinyApp(ui = ui, server = server)Instructions
Using code provided in exercises/ex-06.R (which is the same as demo-09) experiment with {bslib}’s themer tool to explore different themes.
bs_theme() call in the script to reflect the changes you madeTip
Making a good theme can be challenging, instead try making the ugliest possible app. This is a lot easier and more fun and just as instructive.
12:00
Natural language chat powered by LLMs
Does not provide the LLM direct access to raw data
It can only read or filter data by writing SQL SELECT statements
Leverages DuckDB for its SQL engine
demos/demo-12.R
library(dplyr)
library(ggplot2)
library(shiny)
library(bslib)
library(querychat)
library(plotly)
d <- readr::read_csv(here::here("data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
d_qc <- QueryChat$new(d)
ui <- page_sidebar(
title = "Weather Data",
sidebar = d_qc$sidebar(),
card(
card_header(
textOutput("title"),
popover(
bsicons::bs_icon("gear", title = "Settings"),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
class = "d-flex justify-content-between align-items-center"
),
card_body(
plotlyOutput("plot")
),
full_screen = TRUE
),
uiOutput("valueboxes")
)
server <- function(input, output, session) {
d_qc_vals <- d_qc$server()
output$valueboxes <- renderUI({
clean <- function(x) {
round(x, 1) |> paste("°C")
}
layout_columns(
value_box(
title = "Average Temp",
value = mean(d_qc_vals$df()$temp_avg, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-half"),
theme = "success"
),
value_box(
title = "Minimum Temp",
value = min(d_qc_vals$df()$temp_min, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-snow"),
theme = "primary"
),
value_box(
title = "Maximum Temp",
value = max(d_qc_vals$df()$temp_max, na.rm = TRUE) |> clean(),
showcase = bsicons::bs_icon("thermometer-sun"),
theme = "danger"
)
)
})
output$title <- renderText({
names(d_vars)[d_vars == input$var]
})
output$plot <- renderPlotly({
p <- d_qc_vals$df() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line(mapping = aes(group = airport_code)) +
theme_minimal()
ggplotly(p)
})
}
shinyApp(ui = ui, server = server)Instructions
Go to Posit Connect Cloud and sign up for an account if you don’t have one already.
If asked to pick a plan, use the Free option (more than sufficient for our needs)
03:00
For deployment, generally apps will be organized as a single folder that contains all the necessary components (R script, data files, other static content).
www/ subfolderapp.RInstructions
Package up ex-07.R as an app in exercises/ex-07 (you will need to create this folder)
weather.csv) into this folderCreate a dependency file for your app using rsconnect::writeManifest()
If you use RStudio IDE
10:00
Shinylive + a LLM (with Shiny specific context) that can help you start developing a Shiny app
Also accessible within Positron via Positron Assistant
A curated list of awesome R packages that offer extended UI or server components for the R web framework Shiny.

Learn more at github.com/nanxstats/awesome-shiny-extensions
renderUI()bs_theme() and {thematic}