Interactive reporting with Shiny III

Notes
Modified

May 13, 2026

NoteLearning objectives
  • Utilize {bslib} for modern Shiny UI design
  • Use cards and value boxes to enhance the UI
  • Implement dynamic UIs with renderUI()
  • Apply dynamic theming with bs_theme() and {thematic}
  • Deploy a Shiny app to Posit Connect Cloud

Modern UIs with {bslib}

Shiny and Bootstrap

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.

{bslib} components

Beyond layout functions, {bslib} provides several UI components that are commonly seen in modern web interfaces:

  • Cards — bordered rectangular containers for grouping related content
  • Value boxes — styled boxes for displaying key metrics
  • Sidebars — responsive panel for inputs or navigation
  • Popovers & tooltips — contextual info on hover or click

Cards

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")
  )
)

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"
  )
)
1
"bg-primary" applies Bootstrap’s primary color as a background — defaults to blue in most themes.
2
"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."
  )
)

Value boxes

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")
)
1
bs_icon() from {bsicons} provides Bootstrap Icons — a library of 2000+ SVG icons.
2
theme = "success" applies Bootstrap’s success color (green). Other options: "primary", "secondary", "info", "warning", "danger".

Value boxes are typically displayed side by side using layout_columns().

Dynamic UIs

The problem with static value boxes

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"
)
1
This works but requires a separate 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:

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"
      )
    )
  })
}
1
uiOutput("valueboxes") is a placeholder in the UI — it starts empty.
2
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.

Theming

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,
  # ...
)
1
Bootstrap version (5 is current).
2
A Bootswatch preset name. Browse the full gallery at https://bootswatch.com/.
3
Override accent colors directly — here setting Cornell carnelian as primary.
4
font_google() loads a Google Font.

Dynamic theming with input_dark_mode()

{bslib} provides input_dark_mode() to add a light/dark toggle to the UI:

ui <- page_sidebar(
  title = "Weather Data",
  theme = bs_theme(preset = "shiny"),
  sidebar = sidebar(
    input_dark_mode(id = "dark_mode"),
    # ... other inputs ...
  ),
  # ...
)
1
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.

{thematic} for consistent plot theming

When the UI theme changes, {ggplot2} plots don’t automatically update — their colors are set at render time. The {thematic} package bridges this gap:

library(thematic)
thematic_shiny()

ui <- page_sidebar(...)
server <- function(input, output, session) {
  ...
}
shinyApp(ui, server)
1
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.

{querychat}: natural language interfaces

{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:

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)
1
QueryChat$new() initializes the querychat object with a data frame. It introspects the schema to tell the LLM what columns are available.
2
qc$sidebar() inserts the chat interface as a sidebar panel.
3
qc$server() registers the reactive logic on the server side.
4
qc_vals$df() returns the filtered data frame in response to the reader’s query.

Key design properties: - The LLM never sees raw data — only schema information and the SQL it generates - All filtering is done via SQL SELECT statements (using DuckDB as the engine) - This makes it more reliable and auditable than giving an LLM direct data access

Deploying Shiny apps

Organizing your app for deployment

Shiny apps for deployment are typically packaged as a folder containing all necessary components:

  • app.R — the main Shiny script (named exactly app.R)
  • Data files — in the same folder or a subfolder
  • www/ — static files (CSS, images, JS) served directly to the browser
  • renv.lock — package dependency snapshot (for reproducibility)

Common pitfalls: - Absolute paths — paths like /Users/you/data/weather.csv break on a remote server. Use relative paths from the app folder root. - Too many dependencies — audit your library() calls; load only what the app actually needs - Secrets — API keys and passwords must not be hard-coded in app.R; use environment variables or a secure secrets manager

Deployment options

Posit Connect Cloud (recommended for this course): free tier available; deploy via Posit Publisher or the {rsconnect} package.

rsconnect::writeManifest()   # generates a deployment manifest with package versions

Then use the Posit Publisher IDE extension or:

rsconnect::deployApp(appDir = "exercises/ex-07", appName = "my-weather-app")

Other options: - Shinylive — runs Shiny entirely in the browser (using WebAssembly); no server required, but limited to packages that support Wasm - Shiny Server — open-source server you host yourself - shinyapps.io — Posit’s hosted service (free tier available)

Getting help

Note Application Exercise

AE 23: Building a Shiny weather app

Summary

  • Cards (card(), card_header(), card_body()) and value boxes (value_box()) from {bslib} provide modular, Bootstrap-based UI components
  • uiOutput() / renderUI() generate complete UI blocks reactively — useful when the number, content, or style of elements depends on the data
  • bs_theme() controls the Bootstrap theme; input_dark_mode() adds a light/dark toggle; thematic_shiny() propagates the theme to {ggplot2} plots
  • {querychat} enables natural language filtering by translating plain-English questions into SQL — reliable because the LLM never touches raw data
  • Deploy to Posit Connect Cloud using Posit Publisher or the {rsconnect} package; use relative paths, minimize dependencies, and store secrets as environment variables

Acknowledgements

Material 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.