Skip to content
Back to skills

2573 Dark Mode 77760605

ASecurity

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

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 11, 2026
datagoreactperformance

Works with

  • cli

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 2573-dark-mode_77760605 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2573 Dark Mode 77760605?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 2573 Dark Mode 77760605
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2573-dark-mode-77760605/badge)](https://www.skillsdirectory.com/skills/tools-only-2573-dark-mode-77760605)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# 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)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.