card(
card_header("A header"),
card_body(
markdown("Some **bold** text")
)
)Some bold text
ae-shiny (repo name will be suffixed with your GitHub name).renderUI()bs_theme() and {thematic}Much of the interface provided by Shiny is based on Bootstrap — a widely used HTML/CSS/JavaScript framework. Shiny widgets, page layouts, and default styling all come from Bootstrap. {bslib} is the package that controls which Bootstrap version is used and how it is customized.
Bootstrap’s influence also appears in Quarto HTML documents, which use Bootstrap for their styling. This means concepts like Bootstrap classes ("bg-primary", "text-danger") apply in both Shiny apps and Quarto HTML output.
Beyond layout functions, {bslib} provides several UI components that are commonly seen in modern web interfaces:
A card groups related elements with a visual border, optional header, and scrollable body:
card(
card_header("A header"),
card_body(
markdown("Some **bold** text")
)
)Some bold text
Cards accept Bootstrap utility classes on the class argument:
card(
max_height = 250,
card_header(
"Long scrollable text",
class = "bg-primary"
),
card_body(
"... lots of content ...",
class = "bg-info"
)
)"bg-primary" applies Bootstrap’s primary color as a background — defaults to blue in most themes.
"bg-info" applies the info color (typically cyan). See Bootstrap’s background utilities for options.
A card can also hold multiple card_body() sections, useful for mixing content types:
card(
card_header("Text and a map!"),
card_body(
max_height = 200,
class = "p-0",
leaflet::leaflet() |> leaflet::addTiles()
),
card_body(
"Some explanatory text below the map."
)
)Here is a complete weather app that wraps the plot in a card() with a reactive title in the card_header():
demos/demo-07.R
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()
})
}card_header() takes reactive output (textOutput()) to display a dynamic title.
renderText() looks up the human-readable label from d_vars using the selected variable name.
Value boxes display a single key metric with a label, icon, and optional detail text:
value_box(
title = "Average Temp",
value = "12.4°C",
showcase = bs_icon("thermometer-half"),
theme = "success",
p("Across all years in the dataset")
)bs_icon() from {bsicons} provides Bootstrap Icons — a library of 2000+ SVG icons.
theme = "success" applies Bootstrap’s success color (green). Other options: "primary", "secondary", "info", "warning", "danger".
Average Temp
12.4°C
Across all years in the dataset
Value boxes are typically displayed side by side using layout_columns().
A naive approach to displaying a dynamic temperature in a value box:
value_box(
title = "Average Temp",
value = textOutput("avgtemp"),
showcase = bs_icon("thermometer-half"),
theme = "success"
)textOutput() / renderText() pair for every value. If you want to change the theme or icon based on the value (e.g., red when hot, blue when cold), the static theme = argument cannot respond to data.
uiOutput() and renderUI()renderUI() generates complete UI elements from within the server function, making them fully reactive:
demos/demo-08.R
ui <- page_sidebar(
title = "Weather Data",
sidebar = sidebar(
selectInput(
"region",
"Region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput("name", "Airport", choices = c()),
selectInput("var", "Variable", choices = d_vars, selected = "temp_avg")
),
card(
card_header(textOutput("title")),
card_body(plotOutput("plot"))
),
uiOutput("valueboxes")
)
server <- function(input, output, session) {
# ... observe and d_city reactive as before ...
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 = bs_icon("thermometer-half"),
theme = "success"
),
value_box(
title = "Minimum Temp",
value = min(d_city()$temp_min, na.rm = TRUE) |> clean(),
showcase = bs_icon("thermometer-snow"),
theme = "primary"
),
value_box(
title = "Maximum Temp",
value = max(d_city()$temp_max, na.rm = TRUE) |> clean(),
showcase = bs_icon("thermometer-sun"),
theme = "danger"
)
)
})
}uiOutput("valueboxes") is a placeholder in the UI — it starts empty.
renderUI({...}) builds the entire layout of three value boxes inside the server. Because d_city() is used here, the entire block re-renders whenever the selected airport changes, and computed values like mean(...) are automatically reactive.
uiOutput() / renderUI() is the right tool whenever the number of UI elements or their properties (theme, icon, structure) depend on the data.
Due to the ubiquity of Bootstrap, a large amount of community effort has already gone into developing custom themes.
bs_theme()bs_theme() creates a Bootstrap theme object that can be passed to the theme argument of any page function:
my_theme <- bs_theme(
version = 5,
preset = "lux",
bg = "#ffffff",
fg = "#000000",
primary = "#B31B1B",
base_font = font_google("Roboto")
)
ui <- page_sidebar(
theme = my_theme,
# ...
)font_google() loads a Google Font.
input_dark_mode(){bslib} provides input_dark_mode() to add a light/dark toggle to the UI:
demos/demo-10.R
ui <- page_sidebar(
title = "Weather Data",
theme = bs_theme(preset = "shiny"),
sidebar = sidebar(
input_dark_mode(id = "dark_mode"),
# ... other inputs ...
),
# ...
)input_dark_mode() adds a toggle switch that switches between the theme’s light and dark variants. id = "dark_mode" makes the current state available as input$dark_mode in the server.
When the UI theme changes, {ggplot2} plots don’t automatically update — their colors are set at render time. The {thematic} package bridges this gap:
demos/demo-11.R
library(thematic)
thematic_shiny()
ui <- page_sidebar(...)
server <- function(input, output, session) {
...
}
shinyApp(ui, server)thematic_shiny() called before shinyApp() automatically extracts the app’s Bootstrap theme colors and applies them to all {ggplot2} plots. When the user switches to dark mode, the plot background, text, and grid lines update to match.
The full working app with dynamic theming is in demos/demo-11.R.
Open exercises/ex-06.R (which is the same as demo-09.R) and experiment with {bslib}’s interactive theme builder by calling bslib::bs_themer() in the server.
Try:
Based on the output in your console, update the bs_theme() call in the script to reflect the changes you made.
Making a good theme can be challenging. Try making the ugliest possible app — it’s easier and just as instructive.
{querychat} adds a natural language chat interface to a Shiny app. The reader types questions in plain English; {querychat} translates them into SQL SELECT statements against the dataset using a language model, then filters the data and updates all reactive outputs:
demos/demo-12.R
library(shiny)
library(bslib)
library(querychat)
qc <- QueryChat$new(penguins)
ui <- page_sidebar(
sidebar = qc$sidebar(),
tableOutput("table"),
plotOutput("plot")
)
server <- function(input, output, session) {
qc_vals <- qc$server()
output$table <- renderTable({
qc_vals$df()
})
}
shinyApp(ui, server)QueryChat$new() initializes the querychat object with a data frame. It introspects the schema to tell the LLM what columns are available.
qc$sidebar() inserts the chat interface as a sidebar panel.
qc$server() registers the reactive logic on the server side.
qc_vals$df() returns the filtered data frame in response to the reader’s query.
Key design properties:
SELECT statements (using DuckDB as the engine)Shiny apps for deployment are typically packaged as a folder containing all necessary components:
app.R — the main Shiny script (named exactly app.R)www/ — static files (CSS, images, JS) served directly to the browserCommon pitfalls include:
/Users/you/data/weather.csv break on a remote server. Use relative paths from the app folder root.library() calls; load only what the app actually needsapp.R; use environment variables or a secure secrets managerPosit Connect Cloud (recommended for this course): free tier available; deploy via Posit Publisher or the {rsconnect} package.
First, create a deployment manifest to capture package versions:
rsconnect::writeManifest() # generates a deployment manifest with package versionsThen use the Posit Publisher extension to publish, or from the R console:
rsconnect::deployApp(appDir = "...", appName = "...")Other options include:
Go to Posit Connect Cloud and sign up for an account (use the Free plan).
Package up your app as a self-contained folder exercises/ex-07/ containing:
app.R — your Shiny scriptdata/weather.csv — the data fileCreate a dependency manifest: rsconnect::writeManifest(appDir = "exercises/ex-07")
Set up your deployment credentials using Posit Publisher and deploy the app.
Pay attention to file paths — absolute paths will break on the remote server. Use paths relative to the app folder root.
card(), card_header(), card_body()) and value boxes (value_box()) from {bslib} provide modular, Bootstrap-based UI componentsuiOutput() / renderUI() generate complete UI blocks reactively — useful when the number, content, or style of elements depends on the databs_theme() controls the Bootstrap theme; input_dark_mode() adds a light/dark toggle; thematic_shiny() propagates the theme to {ggplot2} plotsMaterial derived in part from posit::conf 2025 - Shiny for R Workshop by Colin Rundel (CC BY 4.0), Programming with LLMs (CC BY 4.0), and Advanced Data Visualization.