Claude Skill

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

LLM Mart · 0 points · 20 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download posit-dev-skills-shiny_shiny-bslib-theming-b58a92e.zip · 12 KB
Part of posit-dev/skills — 23 skills

Install

skills CLI npx skills add https://github.com/posit-dev/skills/tree/main/shiny/shiny-bslib-theming
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
Git 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

  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:

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

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

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:

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

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

No comments yet.

Reviews (0)

No reviews yet.

Related