shiny-bslib-theming
Advanced theming for Shiny apps using bslib and Bootstrap 5. Use when customizing app appearance with bs_theme(), Bootswatch themes, custom colors, typography, brand.yml integration, Bootstrap Sass variables, custom Sass/CSS rules, dark mode and color modes, dynamic theme switchi
Install
npx skills add https://github.com/posit-dev/skills/tree/main/shiny/shiny-bslib-theming
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
git clone https://github.com/posit-dev/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole posit-dev/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Theming Shiny Apps with bslib
Customize Shiny app appearance using bslib's Bootstrap 5 theming system. From quick Bootswatch themes to advanced Sass customization and dynamic color mode switching.
Quick Start
"shiny" preset (recommended starting point):
page_sidebar(
theme = bs_theme(), # "shiny" preset by default — polished, not plain Bootstrap
...
)
Bootswatch theme (for a different visual style):
page_sidebar(
theme = bs_theme(preset = "zephyr"), # or "cosmo", "minty", "darkly", etc.
...
)
Custom colors and fonts:
page_sidebar(
theme = bs_theme(
version = 5,
bg = "#FFFFFF",
fg = "#333333",
primary = "#2c3e50",
base_font = font_google("Lato"),
heading_font = font_google("Montserrat")
),
...
)
Auto-brand from _brand.yml:
If a _brand.yml file exists in your app or project directory, bs_theme() automatically discovers and applies it. No code changes needed. Requires the brand.yml R package.
bs_theme(brand = FALSE) # Disable auto-discovery
bs_theme(brand = TRUE) # Require _brand.yml (error if not found)
bs_theme(brand = "path/to/brand.yml") # Explicit path
Theming Workflow
- Start with the
"shiny"preset (default) or a Bootswatch theme close to your desired look - Customize main colors (
bg,fg,primary) - Adjust fonts with
font_google()or other font helpers - Fine-tune with Bootstrap Sass variables via
...orbs_add_variables() - Add custom Sass rules with
bs_add_rules()if needed - Enable
thematic::thematic_shiny()so plots match the theme - Use
bs_themer()during development for interactive preview
Example:
theme <- bs_theme(preset = "minty") |>
bs_theme_update(
primary = "#1a9a7f",
base_font = font_google("Lato")
) |>
bs_add_rules("
.card { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
")
bs_theme()
Central function for creating Bootstrap themes. Returns a sass::sass_bundle() object.
bs_theme(
version = version_default(),
preset = NULL, # "shiny" (default for BS5+), "bootstrap", or Bootswatch name
..., # Bootstrap Sass variable overrides
brand = NULL, # brand.yml: NULL (auto), TRUE (require), FALSE (disable), or path
bg = NULL, fg = NULL,
primary = NULL, secondary = NULL,
success = NULL, info = NULL, warning = NULL, danger = NULL,
base_font = NULL, code_font = NULL, heading_font = NULL,
font_scale = NULL, # Scalar multiplier for base font size (e.g., 1.5 = 150%)
bootswatch = NULL # Alias for preset
)
Use bs_theme_update(theme, ...) to modify an existing theme. Use is_bs_theme(x) to test if an object is a theme.
Presets and Bootswatch
The "shiny" preset (recommended): bs_theme() defaults to preset = "shiny" for Bootstrap 5+. This is a polished, purpose-built theme designed specifically for Shiny apps — it is not plain Bootstrap. It provides professional styling with well-chosen defaults for cards, sidebars, value boxes, and other bslib components. Start here and customize with colors and fonts before reaching for a Bootswatch theme.
Vanilla Bootstrap: Use preset = "bootstrap" to remove the "shiny" preset and get unmodified Bootstrap 5 styling.
Built-in presets: builtin_themes() lists bslib's own presets.
Bootswatch themes: bootswatch_themes() lists all available Bootswatch themes. Choose one that fits the app's purpose and audience — don't apply one by default.
Popular options: "zephyr" (light, modern), "cosmo" (clean), "minty" (fresh green), "flatly" (flat design), "litera" (crisp), "darkly" (dark), "cyborg" (dark), "simplex" (minimalist), "sketchy" (hand-drawn).
Main Colors
The most influential colors — changing these affects hundreds of CSS rules via variable cascading:
| Parameter | Description |
|---|---|
bg |
Background color |
fg |
Foreground (text) color |
primary |
Primary brand color (links, nav active states, input focus) |
secondary |
Default for action buttons |
success |
Positive/success states (typically green) |
info |
Informational content (typically blue-green) |
warning |
Warnings (typically yellow) |
danger |
Errors/destructive actions (typically red) |
bs_theme(
bg = "#202123", fg = "#B8BCC2",
primary = "#EA80FC", secondary = "#48DAC6"
)
Color tips:
bg/fg: similar hue, large luminance difference (ensure contrast for readability)primary: contrasts with bothbgandfg; used for hyperlinks, navigation, input focus- Colors can be any format
htmltools::parseCssColors()understands
Typography
Three font arguments: base_font, heading_font, code_font. Use font_scale to uniformly scale all font sizes (e.g., 1.5 for 150%).
Each argument accepts a single font, a font_collection(), or a character vector of font names.
font_google()
Downloads and caches Google Fonts locally (local = TRUE by default). Internet needed only on first download.
bs_theme(
base_font = font_google("Roboto"),
heading_font = font_google("Montserrat"),
code_font = font_google("Fira Code")
)
With variable weights: font_google("Crimson Pro", wght = "200..900")
With specific weights: font_google("Raleway", wght = c(300, 400, 700))
Recommend fallbacks to avoid Flash of Invisible Text (FOIT) on slow connections:
bs_theme(
base_font = font_collection(
font_google("Lato", local = FALSE),
"Helvetica Neue", "Arial", "sans-serif"
)
)
Font pairing resource: fontpair.co
font_link()
CSS web font interface for custom font URLs:
font_link("Crimson Pro",
href = "https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@200..900")
font_face()
For locally hosted font files with full @font-face control:
font_face(
family = "Crimson Pro",
style = "normal",
weight = "200 900",
src = "url(fonts/crimson-pro.woff2) format('woff2')"
)
font_collection()
Combine multiple fonts with fallback order:
font_collection(font_google("Lato"), "Helvetica Neue", "Arial", "sans-serif")
Low-Level Theming Functions
For customizations beyond bs_theme()'s named parameters. These work directly with Bootstrap's Sass layers.
bs_add_variables()
Add or override Bootstrap Sass variable defaults:
theme <- bs_add_variables(
bs_theme(preset = "sketchy", primary = "orange"),
"body-bg" = "#EEEEEE",
"font-family-base" = "monospace",
"font-size-base" = "1.4rem",
"btn-padding-y" = ".16rem"
)
The .where parameter controls placement in the Sass compilation order:
.where |
When to use |
|---|---|
"defaults" (default) |
Set variable defaults with !default flag. Placed before Bootstrap's own defaults. |
"declarations" |
Reference other Bootstrap variables (e.g., $secondary). Placed after Bootstrap's defaults. |
"rules" |
Placed after all rules. Rarely needed. |
Referencing Bootstrap variables:
# This fails in bs_theme() because $secondary isn't defined yet:
# bs_theme("progress-bar-bg" = "$secondary")
# Use bs_add_variables with .where = "declarations" instead:
bs_theme() |>
bs_add_variables("progress-bar-bg" = "$secondary", .where = "declarations")
bs_add_rules()
Add custom Sass/CSS rules that can reference Bootstrap variables and mixins:
theme <- bs_theme(primary = "#007bff") |>
bs_add_rules("
.custom-card {
background: mix($bg, $primary, 95%);
border: 1px solid $primary;
padding: $spacer;
@include media-breakpoint-up(md) {
padding: $spacer * 2;
}
}
")
From external file: bs_add_rules(sass::sass_file("www/custom.scss"))
Available Sass functions: lighten(), darken(), mix(), rgba(), color-contrast().
Available Bootstrap mixins: @include media-breakpoint-up(), @include box-shadow(), @include border-radius().
bs_add_functions() and bs_add_mixins()
Add custom Sass functions or mixins to the theme bundle:
theme |>
bs_add_functions("@function my-tint($color) { @return mix(white, $color, 20%); }") |>
bs_add_rules(".highlight { background: my-tint($primary); }")
bs_bundle()
Append sass::sass_bundle() objects to a theme (for packaging reusable theme extensions):
my_extension <- sass::sass_layer(
defaults = list("my-var" = "red !default"),
rules = ".my-class { color: $my-var; }"
)
theme <- bs_theme() |> bs_bundle(my_extension)
Bootstrap Sass Variables
Pass any Bootstrap 5 Sass variable through bs_theme(...) or bs_add_variables().
Finding variable names: https://rstudio.github.io/bslib/articles/bs5-variables/
Common variables:
bs_theme(
"border-radius" = "0.5rem",
"card-border-radius" = "1rem",
"card-bg" = "lighten($bg, 5%)",
"navbar-bg" = "$primary",
"link-color" = "$primary",
"font-size-base" = "1rem",
"spacer" = "1rem",
"btn-padding-y" = ".5rem",
"btn-padding-x" = "1rem",
"input-border-color" = "#dee2e6"
)
Values can be Sass expressions referencing variables, functions, and math.
Bootstrap CSS Custom Properties
See sass-and-css-variables.md for details on:
- How Sass variables compile into
--bs-*CSS custom properties - Runtime vs compile-time variable layers
- How Bootstrap 5.3 color modes use CSS variable overrides
- Per-element theming with
data-bs-theme - CSS utility classes for one-off styling
Dark Mode and Color Modes
See dark-mode.md for details on:
- Bootstrap 5.3's client-side color mode system (
data-bs-themeattribute) input_dark_mode()andtoggle_dark_mode()for user-controlled switching- Server-side theme switching with
session$setCurrentTheme() - Writing custom Sass that works across light/dark modes
- Component compatibility (what responds to theming, what doesn't)
Theming R Plots
bs_theme() only affects CSS. R plot output (rendered server-side as images) won't auto-match. Use the thematic package:
library(thematic)
thematic_shiny(font = "auto") # Call before shinyApp()
shinyApp(ui, server)
- Works with base R, ggplot2, and lattice
- Translates CSS colors into R plotting defaults
font = "auto"also matches fonts frombs_theme()- Complements
bs_themer()for real-time preview
Set global ggplot2 theme for further consistency:
library(ggplot2)
theme_set(theme_minimal())
Dashboard Background Styling
The bslib-page-dashboard CSS class adds a light gray background behind the main content area, giving dashboard-style apps a polished look where cards stand out against the background. This is a theming detail — it doesn't change layout behavior, only the visual treatment.
For page_sidebar() dashboards:
page_sidebar(
class = "bslib-page-dashboard",
title = "My Dashboard",
sidebar = sidebar(...),
...
)
For page_navbar() with dashboard-focused pages:
Apply the class to individual nav_panel() containers (not page_navbar() itself) so only dashboard-oriented pages get the gray background:
page_navbar(
title = "Analytics",
nav_panel("Dashboard", class = "bslib-page-dashboard",
layout_column_wrap(...)
),
nav_panel("Report",
# No dashboard class — standard white background for prose/reports
...
)
)
Interactive Theming Tools
bs_theme_preview()
Standalone demo app for previewing a theme with many example UI components:
bslib::bs_theme_preview() # Default theme
bslib::bs_theme_preview(bs_theme(preset = "darkly")) # Custom theme
Includes the theming UI by default (with_themer = TRUE).
run_with_themer()
Run an existing Shiny app with the theme editor overlay (instead of shiny::runApp()):
run_with_themer(shinyApp(ui, server))
run_with_themer("path/to/app")
bs_themer()
Add the theme editor to your own app's server function:
server <- function(input, output, session) {
bs_themer() # Add during development, remove for production
# ...
}
All three tools print the resulting bs_theme() code to the R console for easy copy-paste. Limitations: Bootstrap 5+ only, Shiny apps and runtime: shiny R Markdown only, doesn't affect 3rd-party widgets that don't use bs_dependency_defer().
Theme Inspection
Retrieve computed Sass variable values:
vars <- c("body-bg", "body-color", "primary", "border-radius")
bs_get_variables(bs_theme(), varnames = vars)
bs_get_variables(bs_theme(preset = "darkly"), varnames = vars)
Check contrast (for accessibility):
bs_get_contrast(bs_theme(), c("primary", "dark", "light"))
Aim for WCAG AA compliance: 4.5:1 for normal text, 3:1 for large text.
Best Practices
- Prefer
bs_theme()over custom CSS -- variables cascade to all related components automatically - Pin Bootstrap version:
bs_theme(version = 5)prevents breakage if defaults change - Use fallback fonts with
font_collection()to avoid FOIT on slow connections - Test across components: inputs, buttons, cards, navs, plots, tables, modals, toasts, mobile
- Check accessibility with
bs_get_contrast()and browser dev tools - Use CSS utility classes for one-off styling instead of custom CSS (see sass-and-css-variables.md)
- Organize complex themes in a separate
theme.R:
# theme.R
app_theme <- function() {
bs_theme(
version = 5,
primary = "#2c3e50",
base_font = font_google("Lato"),
heading_font = font_google("Montserrat", wght = c(400, 700))
) |>
bs_add_rules(sass::sass_file("www/custom.scss"))
}
Reference Files
- sass-and-css-variables.md -- Bootstrap's two-layer variable system, CSS custom properties, utility classes
- dark-mode.md -- Color modes, dark mode, dynamic theming, component compatibility
Files (skills)
-
references
-
dark-mode.md 10.5 KB
# Dark Mode and Color Modes Bootstrap 5.3 introduced client-side color modes that switch CSS custom properties without Sass recompilation. bslib integrates this via `input_dark_mode()` and `toggle_dark_mode()`. ## Table of Contents - [How Bootstrap Color Modes Work](#how-bootstrap-color-modes-work) - [input_dark_mode()](#input_dark_mode) - [toggle_dark_mode()](#toggle_dark_mode) - [Server-Side Theme Switching](#server-side-theme-switching) - [Client-Side vs Server-Side](#client-side-vs-server-side) - [Custom Styles Across Modes](#custom-styles-across-modes) - [Component Compatibility](#component-compatibility) - [Performance](#performance) - [Best Practices for Dark-Mode-Compatible Themes](#best-practices-for-dark-mode-compatible-themes) ## How Bootstrap Color Modes Work Bootstrap 5.3 uses the `data-bs-theme` attribute on HTML elements to switch color modes. When toggled, Bootstrap overrides a set of CSS custom properties (`--bs-body-bg`, `--bs-body-color`, `--bs-emphasis-color`, etc.) without any Sass recompilation. **Global mode** (set on `<html>`): ```html <html data-bs-theme="light"> <!-- or "dark" --> ``` **Per-element override** (scoped to a component): ```r # This card is always dark, regardless of global mode tags$div( `data-bs-theme` = "dark", card(card_header("Settings"), "Always dark card") ) ``` **Default behavior:** If no `data-bs-theme` is set, Bootstrap respects the user's OS-level `prefers-color-scheme` preference. ## input_dark_mode() A toggle button that switches between Bootstrap 5.3's light and dark color modes client-side. ```r input_dark_mode( ..., # Additional HTML attributes (class, style, etc.) id = NULL, # Input ID to reactively read current mode mode = NULL # Initial mode: NULL (follow OS), "light", or "dark" ) ``` **Placing in a `page_navbar()` header:** Wrap `input_dark_mode()` in `nav_item()` (which places arbitrary HTML in the navbar) and precede it with `nav_spacer()` (which pushes all following items to the far right): ```r page_navbar( title = "My App", nav_panel("Dashboard", ...), nav_panel("Analysis", ...), nav_spacer(), # pushes everything after it to the right nav_item(input_dark_mode(id = "color_mode")) # toggle button, far-right of navbar ) ``` `input_dark_mode()` can also be placed in a `sidebar()` or anywhere else in the UI without any wrapper. **Enabling dark mode without a visible toggle:** Pass `style = css(display = "none")` to activate Bootstrap's color mode system without rendering a button. The app follows the user's OS preference (`prefers-color-scheme`) by default and can still be driven from the server with `toggle_dark_mode()`: ```r page_navbar( title = "My App", nav_item(input_dark_mode(id = "color_mode", style = css(display = "none"))), nav_panel("Dashboard", ...) ) ``` Use this when you want OS-aware dark mode or server-controlled mode changes but don't want to expose a toggle in the UI. Include an `id` if the server needs to react to or control the mode; omit it if you only want passive OS-following behavior. **Reading the current mode in the server:** ```r output$mode_text <- renderText({ paste("Current mode:", input$color_mode) # "light" or "dark" }) ``` **How it works under the hood:** 1. User clicks the toggle 2. `input_dark_mode()` sets `data-bs-theme="dark"` (or `"light"`) on `<html>` 3. Bootstrap's pre-compiled CSS variable overrides take effect immediately 4. All Bootstrap components and utilities update their colors 5. If `id` is provided, the server receives `"light"` or `"dark"` **No Sass recompilation happens** — this is purely a CSS variable switch, making it instantaneous. ## toggle_dark_mode() Programmatically set or toggle the color mode from the server: ```r toggle_dark_mode( mode = NULL, # "light", "dark", or NULL to toggle session = get_current_session() ) ``` **Examples:** ```r # Toggle between modes observeEvent(input$toggle_btn, { toggle_dark_mode() }) # Force a specific mode observeEvent(input$force_light, { toggle_dark_mode("light") }) ``` ## Server-Side Theme Switching For more extensive theme changes beyond light/dark (different color palettes, fonts, etc.), use `session$setCurrentTheme()`: ```r server <- function(input, output, session) { corporate_theme <- bs_theme( bg = "#FFFFFF", fg = "#212529", primary = "#003366", base_font = font_google("Open Sans") ) playful_theme <- bs_theme( bg = "#FFF8E7", fg = "#333333", primary = "#FF6B35", base_font = font_google("Nunito") ) observeEvent(input$theme_choice, { theme <- switch(input$theme_choice, "corporate" = corporate_theme, "playful" = playful_theme ) session$setCurrentTheme(theme) }) } ``` This triggers a full Sass recompilation and CSS replacement via Shiny's connection. ## Client-Side vs Server-Side | Aspect | `input_dark_mode()` / `toggle_dark_mode()` | `session$setCurrentTheme()` | |---|---|---| | **Mechanism** | Sets `data-bs-theme` attribute (CSS variable swap) | Full Sass recompilation + CSS replacement | | **Speed** | Instantaneous | Noticeable delay | | **Scope** | Light/dark mode only (same Sass, different CSS vars) | Any theme change (colors, fonts, variables) | | **Custom Sass** | Only works if styles use CSS custom properties | Custom Sass is recompiled with new values | | **3rd-party widgets** | Only if they use Bootstrap CSS variables | Only if they use `bs_dependency_defer()` | **Recommendation:** Use `input_dark_mode()` for simple light/dark toggling (faster, no server round-trip). Use `session$setCurrentTheme()` when you need fundamentally different themes. ## Custom Styles Across Modes ### Using Sass Variables (recompiled themes) When using `session$setCurrentTheme()`, Sass variables adapt automatically: ```r custom_rules <- " .custom-card { background: mix($bg, $primary, 95%); border: 1px solid $primary; } " light_theme <- bs_theme(bg = "#FFFFFF", fg = "#212529") |> bs_add_rules(custom_rules) dark_theme <- bs_theme(bg = "#1a1a1a", fg = "#f8f9fa") |> bs_add_rules(custom_rules) ``` ### Using CSS Custom Properties (client-side color modes) When using `input_dark_mode()` (no recompilation), custom CSS must reference CSS custom properties, not Sass variables: ```r bs_theme() |> bs_add_rules(" .custom-card { /* These update automatically when data-bs-theme changes */ background: var(--bs-secondary-bg); color: var(--bs-body-color); border: 1px solid var(--bs-border-color); } ") ``` **Important:** Sass variables like `$bg` and `$primary` are resolved at compile time. They don't change when `data-bs-theme` toggles. Use `var(--bs-*)` properties for styles that should respond to client-side color mode changes. ### Mode-Specific Overrides Target specific modes in custom CSS: ```r bs_theme() |> bs_add_rules(" [data-bs-theme='dark'] .custom-card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); } [data-bs-theme='light'] .custom-card { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } ") ``` ## Component Compatibility ### Responds to bs_theme() and Color Modes **Core Shiny UI:** All inputs, buttons, tables, text, links. **bslib components:** Cards, value boxes, navs/navsets, sidebars, accordions, tooltips, popovers, toasts. ### Not Themeable - **`renderPlot()`** without the `thematic` package (images are rendered server-side, unaffected by CSS) - **HTML widgets with baked-in styles** (hardcoded CSS overrides Bootstrap) - **External iframes** - **Custom HTML with hardcoded inline styles** ### R Plots with Dark Mode The `thematic` package responds to `session$setCurrentTheme()` but **not** to client-side `data-bs-theme` toggles. For apps using `input_dark_mode()`, you may need to re-render plots when the mode changes: ```r output$plot <- renderPlot({ # Re-render when color mode changes mode <- input$color_mode bg <- if (identical(mode, "dark")) "#212529" else "#ffffff" fg <- if (identical(mode, "dark")) "#dee2e6" else "#212529" ggplot(data, aes(x, y)) + geom_point(color = fg) + theme_minimal(base_size = 14) + theme( plot.background = element_rect(fill = bg, color = NA), panel.background = element_rect(fill = bg, color = NA), text = element_text(color = fg), axis.text = element_text(color = fg) ) }) ``` ## Performance - **Client-side color modes** (`input_dark_mode()`) are instantaneous — just a CSS variable swap - **Server-side theme switching** (`session$setCurrentTheme()`) triggers Sass recompilation, which can take a noticeable moment for complex themes ## Best Practices for Dark-Mode-Compatible Themes ### Design the Light Theme First Bootstrap derives dark mode from light-mode values. Design and finalize your light theme first, then patch dark mode as needed. ### Patch Theme Colors for Dark Mode Sass theme colors (`$primary`, `$success`, etc.) compile once for both modes. Colors that work on light backgrounds may lack contrast on dark backgrounds. Patch with CSS property overrides: ```r bs_theme(primary = "#2c6fbb") |> bs_add_variables( "primary-dark" = "#5a9fd4", .where = "defaults" ) |> bs_add_rules(" [data-bs-theme='dark'] { --bs-primary: #{$primary-dark}; --bs-primary-rgb: #{to-rgb($primary-dark)}; } ") ``` Using a Sass variable with `!default` placement lets users override `$primary-dark` via `bs_add_variables()`, and Bootstrap's `to-rgb()` derives the RGB components automatically. ### Avoid Hardcoded Colors Hardcoded hex values won't respond to mode changes. Use `var(--bs-*)` properties, or define custom properties that switch per mode: ```r bs_theme() |> bs_add_rules(" :root, [data-bs-theme='light'] { --my-surface: #f0f4f8; --my-surface-text: #1a2b3c; } [data-bs-theme='dark'] { --my-surface: #1e2a38; --my-surface-text: #c8d6e0; } .my-surface { background: var(--my-surface); color: var(--my-surface-text); } ") ``` ### Test Contrast in Both Modes Colors meeting WCAG contrast in light mode may fail in dark mode. Toggle between modes and verify: - Text readability against backgrounds - Primary-colored buttons and links - Borders and dividers visible but not overpowering - Status colors (success, warning, danger) remain distinguishable ### Keep Dark Mode Overrides Minimal Bootstrap's built-in dark derivations handle most components well. Focus overrides on: - Theme colors with confirmed contrast issues in dark mode - Custom components with colors outside Bootstrap's system - Shadows (dark backgrounds need subtler or lighter shadows) -
sass-and-css-variables.md 7.5 KB
# Bootstrap Sass and CSS Variables Bootstrap 5 uses a two-layer variable system: Sass variables (compile-time) and CSS custom properties (runtime). Understanding both layers is key to effective theming with bslib. ## Table of Contents - [Two-Layer Variable System](#two-layer-variable-system) - [Sass Variables (Compile-Time)](#sass-variables-compile-time) - [CSS Custom Properties (Runtime)](#css-custom-properties-runtime) - [How bslib Connects the Layers](#how-bslib-connects-the-layers) - [CSS Utility Classes](#css-utility-classes) ## Two-Layer Variable System ``` Sass Variables ($primary, $body-bg, ...) ↓ compiled by sass package CSS Custom Properties (--bs-primary, --bs-body-bg, ...) ↓ applied at runtime Rendered Styles ``` **Sass variables** control compile-time defaults. When you set `bs_theme(primary = "red")`, bslib places `$primary: red !default` before Bootstrap's own Sass, causing all downstream variables that reference `$primary` to update. **CSS custom properties** (`--bs-*` prefixed) are emitted from the compiled Sass onto `:root`. They enable runtime overrides — including Bootstrap 5.3's color modes — without recompilation. ## Sass Variables (Compile-Time) ### How bs_theme() Uses Sass Variables `bs_theme()` places variable defaults **before** Bootstrap's own defaults in the Sass compilation. Due to Sass's `!default` flag semantics, your values take precedence: ```r # Sets $primary: red !default before Bootstrap processes its files. # All variables referencing $primary (buttons, links, focus, etc.) update. bs_theme(primary = "red") ``` ### Variable Placement with .where The `.where` parameter in `bs_add_variables()` controls where definitions are placed: ``` ┌─────────────────────────────────────────┐ │ "defaults" ← Your !default vars │ ← bs_theme(...) and bs_add_variables() │ Bootstrap's own !default vars │ │ "declarations" ← Your declarations │ ← Can reference $primary, $secondary, etc. │ "rules" ← Your rules │ ← After all variable processing └─────────────────────────────────────────┘ ``` This is why referencing `$secondary` in `bs_theme()` fails (it's not yet defined), but works with `bs_add_variables(.where = "declarations")`. ### Finding Sass Variable Names **Searchable reference:** https://rstudio.github.io/bslib/articles/bs5-variables/ **Categories of commonly used variables:** | Category | Example Variables | |---|---| | **Colors** | `body-bg`, `body-color`, `primary`, `secondary`, `link-color` | | **Typography** | `font-family-base`, `font-size-base`, `line-height-base`, `headings-font-weight` | | **Spacing** | `spacer`, `spacers` (map) | | **Borders** | `border-width`, `border-color`, `border-radius` | | **Cards** | `card-bg`, `card-border-color`, `card-border-radius`, `card-cap-bg` | | **Buttons** | `btn-padding-y`, `btn-padding-x`, `btn-border-radius`, `btn-font-size` | | **Navbar** | `navbar-bg`, `navbar-padding-y`, `navbar-brand-font-size` | | **Inputs** | `input-bg`, `input-border-color`, `input-border-radius`, `input-focus-border-color` | | **Grid** | `grid-gutter-width`, `container-max-widths` (map) | ## CSS Custom Properties (Runtime) ### Root-Level Properties Bootstrap compiles Sass variables into `--bs-*` CSS custom properties on `:root`: ```css :root { --bs-primary: #0d6efd; --bs-primary-rgb: 13, 110, 253; /* RGB triplet for rgba() usage */ --bs-body-color: #212529; --bs-body-bg: #fff; --bs-body-font-family: system-ui, -apple-system, ...; --bs-body-font-size: 1rem; --bs-border-radius: 0.375rem; --bs-emphasis-color: #000; --bs-secondary-color: rgba(33, 37, 41, 0.75); --bs-link-color: #0d6efd; /* ... hundreds more */ } ``` ### Semantic Color Variants Each theme color gets three semantic variants for subtle UI contexts: ```css :root { --bs-primary-text-emphasis: ...; /* Text on subtle backgrounds */ --bs-primary-bg-subtle: ...; /* Subtle backgrounds */ --bs-primary-border-subtle: ...; /* Subtle borders */ } ``` These are heavily used by Bootstrap's alert, badge, and list-group components. ### Component-Level Properties Many Bootstrap components define local CSS variables instead of relying on root variables: ```css .navbar { --bs-navbar-padding-y: 0.5rem; --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65); /* ... */ } ``` This prevents style inheritance issues in nested contexts (e.g., nested tables). ### Color Modes and CSS Variable Overrides Bootstrap 5.3 uses `[data-bs-theme="dark"]` to swap CSS custom properties without recompilation: ```css /* Light mode (default) */ :root { --bs-body-color: #212529; --bs-body-bg: #fff; --bs-emphasis-color: #000; } /* Dark mode — same properties, different values */ [data-bs-theme="dark"] { --bs-body-color: #dee2e6; --bs-body-bg: #212529; --bs-emphasis-color: #fff; --bs-link-color: #6ea8fe; --bs-border-color: #495057; } ``` No class changes needed on components — switching the `data-bs-theme` attribute on an ancestor cascades new values to all descendants automatically. ### Per-Element Theming Apply `data-bs-theme` to any element for scoped theming: ```r # This card renders in dark mode regardless of the page's global mode tags$div( `data-bs-theme` = "dark", card(card_header("Dark Card"), "Always dark") ) ``` ### Prefix All CSS custom properties use `--bs-` prefix (configurable via the `$prefix` Sass variable). ### Limitations Grid breakpoint CSS variables exist but **cannot** be used in media queries (CSS spec constraint). They can be used in other CSS contexts and via JavaScript. ## How bslib Connects the Layers When you call `bs_theme(primary = "red")`: 1. bslib sets `$primary: red !default` in the Sass defaults layer 2. The sass package compiles all Bootstrap Sass (with your overrides) 3. Bootstrap's `_root.scss` emits `--bs-primary: red` (and all derived properties) 4. The compiled CSS is served to the browser When `input_dark_mode()` toggles dark mode: - It sets `data-bs-theme="dark"` on the `<html>` element (client-side) - Bootstrap's pre-compiled dark mode CSS variables take effect - No Sass recompilation happens When `session$setCurrentTheme()` switches themes: - A completely new Sass compilation produces new CSS - The new CSS replaces the old one via Shiny's connection - This is heavier than client-side color mode toggling ## CSS Utility Classes Bootstrap provides utility classes for one-off styling without custom CSS. These use CSS custom properties internally and respond to color mode changes. **Colors:** ```r card_header(class = "bg-primary text-white", "Blue Header") tags$p(class = "text-muted", "Secondary text") tags$span(class = "text-danger fw-bold", "Error!") ``` **Common utilities:** | Category | Examples | |---|---| | **Background** | `bg-primary`, `bg-secondary`, `bg-success`, `bg-danger`, `bg-light`, `bg-dark` | | **Text color** | `text-primary`, `text-secondary`, `text-muted`, `text-white` | | **Spacing** | `p-3` (padding), `m-4` (margin), `px-2`, `mt-3`, `gap-2` | | **Display** | `d-flex`, `d-none`, `d-md-block`, `d-grid` | | **Text** | `text-center`, `text-end`, `fw-bold`, `fs-5`, `text-truncate` | | **Borders** | `border`, `border-primary`, `rounded`, `rounded-3` | | **Flex** | `justify-content-between`, `align-items-center`, `flex-wrap` | **Reference:** https://rstudio.github.io/bslib/articles/utility-classes/
-
-
SKILL.md 14.2 KB
--- name: shiny-bslib-theming description: Advanced theming for Shiny apps using bslib and Bootstrap 5. Use when customizing app appearance with bs_theme(), Bootswatch themes, custom colors, typography, brand.yml integration, Bootstrap Sass variables, custom Sass/CSS rules, dark mode and color modes, dynamic theme switching, real-time theming, theme inspection, or making R plots match the app theme with thematic. metadata: author: Garrick Aden-Buie (@gadenbuie) version: "1.0" license: MIT --- # Theming Shiny Apps with bslib Customize Shiny app appearance using bslib's Bootstrap 5 theming system. From quick Bootswatch themes to advanced Sass customization and dynamic color mode switching. ## Quick Start **"shiny" preset (recommended starting point):** ```r page_sidebar( theme = bs_theme(), # "shiny" preset by default — polished, not plain Bootstrap ... ) ``` **Bootswatch theme (for a different visual style):** ```r page_sidebar( theme = bs_theme(preset = "zephyr"), # or "cosmo", "minty", "darkly", etc. ... ) ``` **Custom colors and fonts:** ```r page_sidebar( theme = bs_theme( version = 5, bg = "#FFFFFF", fg = "#333333", primary = "#2c3e50", base_font = font_google("Lato"), heading_font = font_google("Montserrat") ), ... ) ``` **Auto-brand from `_brand.yml`:** If a `_brand.yml` file exists in your app or project directory, `bs_theme()` automatically discovers and applies it. No code changes needed. Requires the `brand.yml` R package. ```r bs_theme(brand = FALSE) # Disable auto-discovery bs_theme(brand = TRUE) # Require _brand.yml (error if not found) bs_theme(brand = "path/to/brand.yml") # Explicit path ``` ## Theming Workflow 1. Start with the `"shiny"` preset (default) or a Bootswatch theme close to your desired look 2. Customize main colors (`bg`, `fg`, `primary`) 3. Adjust fonts with `font_google()` or other font helpers 4. Fine-tune with Bootstrap Sass variables via `...` or `bs_add_variables()` 5. Add custom Sass rules with `bs_add_rules()` if needed 6. Enable `thematic::thematic_shiny()` so plots match the theme 7. Use `bs_themer()` during development for interactive preview **Example:** ```r theme <- bs_theme(preset = "minty") |> bs_theme_update( primary = "#1a9a7f", base_font = font_google("Lato") ) |> bs_add_rules(" .card { box-shadow: 0 2px 8px rgba(0,0,0,0.1); } ") ``` ## bs_theme() Central function for creating Bootstrap themes. Returns a `sass::sass_bundle()` object. ```r bs_theme( version = version_default(), preset = NULL, # "shiny" (default for BS5+), "bootstrap", or Bootswatch name ..., # Bootstrap Sass variable overrides brand = NULL, # brand.yml: NULL (auto), TRUE (require), FALSE (disable), or path bg = NULL, fg = NULL, primary = NULL, secondary = NULL, success = NULL, info = NULL, warning = NULL, danger = NULL, base_font = NULL, code_font = NULL, heading_font = NULL, font_scale = NULL, # Scalar multiplier for base font size (e.g., 1.5 = 150%) bootswatch = NULL # Alias for preset ) ``` Use `bs_theme_update(theme, ...)` to modify an existing theme. Use `is_bs_theme(x)` to test if an object is a theme. ### Presets and Bootswatch **The "shiny" preset (recommended):** `bs_theme()` defaults to `preset = "shiny"` for Bootstrap 5+. This is a polished, purpose-built theme designed specifically for Shiny apps — it is **not** plain Bootstrap. It provides professional styling with well-chosen defaults for cards, sidebars, value boxes, and other bslib components. Start here and customize with colors and fonts before reaching for a Bootswatch theme. **Vanilla Bootstrap:** Use `preset = "bootstrap"` to remove the "shiny" preset and get unmodified Bootstrap 5 styling. **Built-in presets:** `builtin_themes()` lists bslib's own presets. **Bootswatch themes:** `bootswatch_themes()` lists all available Bootswatch themes. Choose one that fits the app's purpose and audience — don't apply one by default. Popular options: `"zephyr"` (light, modern), `"cosmo"` (clean), `"minty"` (fresh green), `"flatly"` (flat design), `"litera"` (crisp), `"darkly"` (dark), `"cyborg"` (dark), `"simplex"` (minimalist), `"sketchy"` (hand-drawn). ### Main Colors The most influential colors — changing these affects **hundreds** of CSS rules via variable cascading: | Parameter | Description | |---|---| | `bg` | Background color | | `fg` | Foreground (text) color | | `primary` | Primary brand color (links, nav active states, input focus) | | `secondary` | Default for action buttons | | `success` | Positive/success states (typically green) | | `info` | Informational content (typically blue-green) | | `warning` | Warnings (typically yellow) | | `danger` | Errors/destructive actions (typically red) | ```r bs_theme( bg = "#202123", fg = "#B8BCC2", primary = "#EA80FC", secondary = "#48DAC6" ) ``` **Color tips:** - `bg`/`fg`: similar hue, large luminance difference (ensure contrast for readability) - `primary`: contrasts with both `bg` and `fg`; used for hyperlinks, navigation, input focus - Colors can be any format `htmltools::parseCssColors()` understands ### Typography Three font arguments: `base_font`, `heading_font`, `code_font`. Use `font_scale` to uniformly scale all font sizes (e.g., `1.5` for 150%). Each argument accepts a single font, a `font_collection()`, or a character vector of font names. #### font_google() Downloads and caches Google Fonts locally (`local = TRUE` by default). Internet needed only on first download. ```r bs_theme( base_font = font_google("Roboto"), heading_font = font_google("Montserrat"), code_font = font_google("Fira Code") ) ``` With variable weights: `font_google("Crimson Pro", wght = "200..900")` With specific weights: `font_google("Raleway", wght = c(300, 400, 700))` **Recommend fallbacks** to avoid Flash of Invisible Text (FOIT) on slow connections: ```r bs_theme( base_font = font_collection( font_google("Lato", local = FALSE), "Helvetica Neue", "Arial", "sans-serif" ) ) ``` Font pairing resource: fontpair.co #### font_link() CSS web font interface for custom font URLs: ```r font_link("Crimson Pro", href = "https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@200..900") ``` #### font_face() For locally hosted font files with full `@font-face` control: ```r font_face( family = "Crimson Pro", style = "normal", weight = "200 900", src = "url(fonts/crimson-pro.woff2) format('woff2')" ) ``` #### font_collection() Combine multiple fonts with fallback order: ```r font_collection(font_google("Lato"), "Helvetica Neue", "Arial", "sans-serif") ``` ## Low-Level Theming Functions For customizations beyond `bs_theme()`'s named parameters. These work directly with Bootstrap's Sass layers. ### bs_add_variables() Add or override Bootstrap Sass variable defaults: ```r theme <- bs_add_variables( bs_theme(preset = "sketchy", primary = "orange"), "body-bg" = "#EEEEEE", "font-family-base" = "monospace", "font-size-base" = "1.4rem", "btn-padding-y" = ".16rem" ) ``` **The `.where` parameter** controls placement in the Sass compilation order: | `.where` | When to use | |---|---| | `"defaults"` (default) | Set variable defaults with `!default` flag. Placed **before** Bootstrap's own defaults. | | `"declarations"` | Reference other Bootstrap variables (e.g., `$secondary`). Placed **after** Bootstrap's defaults. | | `"rules"` | Placed after all rules. Rarely needed. | **Referencing Bootstrap variables:** ```r # This fails in bs_theme() because $secondary isn't defined yet: # bs_theme("progress-bar-bg" = "$secondary") # Use bs_add_variables with .where = "declarations" instead: bs_theme() |> bs_add_variables("progress-bar-bg" = "$secondary", .where = "declarations") ``` ### bs_add_rules() Add custom Sass/CSS rules that can reference Bootstrap variables and mixins: ```r theme <- bs_theme(primary = "#007bff") |> bs_add_rules(" .custom-card { background: mix($bg, $primary, 95%); border: 1px solid $primary; padding: $spacer; @include media-breakpoint-up(md) { padding: $spacer * 2; } } ") ``` From external file: `bs_add_rules(sass::sass_file("www/custom.scss"))` Available Sass functions: `lighten()`, `darken()`, `mix()`, `rgba()`, `color-contrast()`. Available Bootstrap mixins: `@include media-breakpoint-up()`, `@include box-shadow()`, `@include border-radius()`. ### bs_add_functions() and bs_add_mixins() Add custom Sass functions or mixins to the theme bundle: ```r theme |> bs_add_functions("@function my-tint($color) { @return mix(white, $color, 20%); }") |> bs_add_rules(".highlight { background: my-tint($primary); }") ``` ### bs_bundle() Append `sass::sass_bundle()` objects to a theme (for packaging reusable theme extensions): ```r my_extension <- sass::sass_layer( defaults = list("my-var" = "red !default"), rules = ".my-class { color: $my-var; }" ) theme <- bs_theme() |> bs_bundle(my_extension) ``` ## Bootstrap Sass Variables Pass any Bootstrap 5 Sass variable through `bs_theme(...)` or `bs_add_variables()`. **Finding variable names:** https://rstudio.github.io/bslib/articles/bs5-variables/ **Common variables:** ```r bs_theme( "border-radius" = "0.5rem", "card-border-radius" = "1rem", "card-bg" = "lighten($bg, 5%)", "navbar-bg" = "$primary", "link-color" = "$primary", "font-size-base" = "1rem", "spacer" = "1rem", "btn-padding-y" = ".5rem", "btn-padding-x" = "1rem", "input-border-color" = "#dee2e6" ) ``` Values can be Sass expressions referencing variables, functions, and math. ## Bootstrap CSS Custom Properties See [sass-and-css-variables.md](references/sass-and-css-variables.md) for details on: - How Sass variables compile into `--bs-*` CSS custom properties - Runtime vs compile-time variable layers - How Bootstrap 5.3 color modes use CSS variable overrides - Per-element theming with `data-bs-theme` - CSS utility classes for one-off styling ## Dark Mode and Color Modes See [dark-mode.md](references/dark-mode.md) for details on: - Bootstrap 5.3's client-side color mode system (`data-bs-theme` attribute) - `input_dark_mode()` and `toggle_dark_mode()` for user-controlled switching - Server-side theme switching with `session$setCurrentTheme()` - Writing custom Sass that works across light/dark modes - Component compatibility (what responds to theming, what doesn't) ## Theming R Plots `bs_theme()` only affects CSS. R plot output (rendered server-side as images) won't auto-match. Use the `thematic` package: ```r library(thematic) thematic_shiny(font = "auto") # Call before shinyApp() shinyApp(ui, server) ``` - Works with base R, ggplot2, and lattice - Translates CSS colors into R plotting defaults - `font = "auto"` also matches fonts from `bs_theme()` - Complements `bs_themer()` for real-time preview Set global ggplot2 theme for further consistency: ```r library(ggplot2) theme_set(theme_minimal()) ``` ## Dashboard Background Styling The `bslib-page-dashboard` CSS class adds a light gray background behind the main content area, giving dashboard-style apps a polished look where cards stand out against the background. This is a theming detail — it doesn't change layout behavior, only the visual treatment. **For `page_sidebar()` dashboards:** ```r page_sidebar( class = "bslib-page-dashboard", title = "My Dashboard", sidebar = sidebar(...), ... ) ``` **For `page_navbar()` with dashboard-focused pages:** Apply the class to individual `nav_panel()` containers (not `page_navbar()` itself) so only dashboard-oriented pages get the gray background: ```r page_navbar( title = "Analytics", nav_panel("Dashboard", class = "bslib-page-dashboard", layout_column_wrap(...) ), nav_panel("Report", # No dashboard class — standard white background for prose/reports ... ) ) ``` ## Interactive Theming Tools ### bs_theme_preview() Standalone demo app for previewing a theme with many example UI components: ```r bslib::bs_theme_preview() # Default theme bslib::bs_theme_preview(bs_theme(preset = "darkly")) # Custom theme ``` Includes the theming UI by default (`with_themer = TRUE`). ### run_with_themer() Run an existing Shiny app with the theme editor overlay (instead of `shiny::runApp()`): ```r run_with_themer(shinyApp(ui, server)) run_with_themer("path/to/app") ``` ### bs_themer() Add the theme editor to your own app's server function: ```r server <- function(input, output, session) { bs_themer() # Add during development, remove for production # ... } ``` All three tools print the resulting `bs_theme()` code to the R console for easy copy-paste. **Limitations:** Bootstrap 5+ only, Shiny apps and `runtime: shiny` R Markdown only, doesn't affect 3rd-party widgets that don't use `bs_dependency_defer()`. ## Theme Inspection **Retrieve computed Sass variable values:** ```r vars <- c("body-bg", "body-color", "primary", "border-radius") bs_get_variables(bs_theme(), varnames = vars) bs_get_variables(bs_theme(preset = "darkly"), varnames = vars) ``` **Check contrast (for accessibility):** ```r bs_get_contrast(bs_theme(), c("primary", "dark", "light")) ``` Aim for WCAG AA compliance: 4.5:1 for normal text, 3:1 for large text. ## Best Practices 1. **Prefer `bs_theme()` over custom CSS** -- variables cascade to all related components automatically 2. **Pin Bootstrap version**: `bs_theme(version = 5)` prevents breakage if defaults change 3. **Use fallback fonts** with `font_collection()` to avoid FOIT on slow connections 4. **Test across components**: inputs, buttons, cards, navs, plots, tables, modals, toasts, mobile 5. **Check accessibility** with `bs_get_contrast()` and browser dev tools 6. **Use CSS utility classes** for one-off styling instead of custom CSS (see [sass-and-css-variables.md](references/sass-and-css-variables.md)) 7. **Organize complex themes** in a separate `theme.R`: ```r # theme.R app_theme <- function() { bs_theme( version = 5, primary = "#2c3e50", base_font = font_google("Lato"), heading_font = font_google("Montserrat", wght = c(400, 700)) ) |> bs_add_rules(sass::sass_file("www/custom.scss")) } ``` ## Reference Files - **[sass-and-css-variables.md](references/sass-and-css-variables.md)** -- Bootstrap's two-layer variable system, CSS custom properties, utility classes - **[dark-mode.md](references/dark-mode.md)** -- Color modes, dark mode, dynamic theming, component compatibility
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.