bootstrict vendors Bootstrap 5.3 itself —
bootstrap_version() reports the exact release — and
compiles it with sass, so the
stylesheet reaching the browser is pinned by this package rather than by
whichever theming package happens to be installed. Because the markup
bootstrict emits is exactly the markup Bootstrap
5.3 documents, a designer’s SASS variables restyle every widget with no
per-component work. This article covers the whole theming story:
building a theme, the designer hand-off via a
_variables.scss sheet, and Bootstrap 5.3 colour modes.
The workflow bootstrict is built around:
_variables.scss sheet — a list of
$name: value; overrides.bootstrict_theme()bootstrict_theme() collects the SASS variable overrides
to compile the vendored Bootstrap tree with. It merges, in order of
increasing priority:
variables=),....bootstrict_theme(
..., # Sass variables, e.g. primary = "#ff6600"
variables = NULL # path to a .scss sheet, or a named list
)Inline ... variables win over the file
on a name clash — handy for overriding a single token without editing
the designer’s sheet:
Any Bootstrap SASS variable works here, named without its
$. Note that some of those names are not valid R names, so
quote them:
Pass the resulting theme to any page constructor:
_variables.scss sheetA _variables.scss sheet is plain SCSS: one
$name: value; declaration per line. It is exactly what a
designer exports, and it drops in unchanged.
// _variables.scss
$primary: #ff6600;
$secondary: #6c757d;
$border-radius: 0.5rem;
$font-family-base: "Inter", sans-serif;parse_scss_variables()parse_scss_variables() turns a sheet’s top-level
declarations into a named list.
bootstrict_theme(variables=) calls it for you, but it is
useful on its own — for example to echo the active theme back in an
app.
tmp <- tempfile(fileext = ".scss")
writeLines(
c("$primary: #ff6600;", "$border-radius: 0.5rem !default;"),
tmp
)
parse_scss_variables(tmp)
#> $primary
#> [1] "#ff6600"
#>
#> $`border-radius`
#> [1] "0.5rem"It strips !default / !global flags and
comments. Note the parser reads the SCSS syntax
($name: value;, with semicolons) — the indented
.sass syntax has no semicolons and cannot be parsed.
Bootstrap 5.3 introduced native light/dark colour
modes driven by the data-bs-theme attribute.
bootstrict exposes them at three levels.
set_bs_color_mode() toggles the whole page between
"light" and "dark" at runtime:
server <- function(input, output, session) {
observeEvent(input$dark_switch, {
set_bs_color_mode(if (isTRUE(input$dark_switch)) "dark" else "light")
})
}A complete light/dark toggle is just a switch wired to
set_bs_color_mode():
ui <- bs_page(
color_mode = "light",
bs_container(
bs_switch_input("dark", "Dark mode"),
bs_card(bs_card_body("This surface follows the colour mode."))
)
)
server <- function(input, output, session) {
observeEvent(input$dark, {
set_bs_color_mode(if (isTRUE(input$dark)) "dark" else "light")
})
}
shinyApp(ui, server)color_mode = "auto" resolves the mode from the OS
(prefers-color-scheme) before the page paints, so there is
no flash of the wrong theme. A mode the user later chooses is remembered
in the browser and wins over the initial value on the next visit;
set_bs_color_mode("auto") forgets it and hands control back
to the OS.
Whatever the preference, the mode actually in force is reported as
input$bootstrict_color_mode, always "light" or
"dark" – never "auto" – so the server can
render to match.
ui <- bs_page(
color_mode = "auto",
bs_container(
bs_radio_button_input(
"mode",
"Appearance",
c(Light = "light", Dark = "dark", Auto = "auto"),
selected = "auto"
),
plotOutput("plot")
)
)
server <- function(input, output, session) {
observeEvent(input$mode, set_bs_color_mode(input$mode))
output$plot <- renderPlot({
# Follows the resolved mode, not the preference.
bg <- if (identical(input$bootstrict_color_mode, "dark")) "black" else "white"
plot(1, bg = bg)
})
}An app that never mentions color_mode is left alone: it
keeps Bootstrap’s default rather than quietly following the OS.
Many components accept a component-level dark = TRUE (or
theme = "dark") argument, which emits
data-bs-theme on just that element — the 5.3 idiom that
replaces the deprecated .navbar-dark /
.dropdown-menu-dark / .carousel-dark classes.
For example a dark navbar on a light page:
Bootstrap’s utility classes
– spacing, colour, display, flex, borders, text – are the other half of
what a designer works with, and bootstrict deliberately exposes none of
them as arguments. Every constructor takes a trailing class
and forwards named ... as HTML attributes, so a utility is
written as itself:
bs_card(
bs_card_body("Padded, centred, muted."),
class = "p-4 text-center text-body-secondary"
)
bs_row(
bs_col("Left", md = 6),
bs_col("Right", md = 6),
class = "gy-3 align-items-center"
)That is the whole story: there is no padding = argument
to learn, and the mockup’s class list transfers verbatim. It also means
a utility that Bootstrap adds in a future release works here on the day
it ships.
The Utility API belongs to the theme rather than to the markup: extend or disable utilities from the SASS sheet, alongside the variables.
$utilities: map-merge(
$utilities,
("cursor": (property: cursor, class: cursor, values: pointer grab))
);parse_scss_variables() reads that map like any other
declaration, so it reaches bootstrict_theme() with the rest
of the sheet.
If you scaffold Shiny apps with golem,
use_bootstrict_golem is a project_hook that
turns a fresh golem skeleton into a minimal bootstrict app:
it rewrites R/app_ui.R to use bs_page() /
bs_container() and creates
inst/app/www/_variables.scss for you.
Page constructors (bs_page() and friends) wire in the
bootstrict dependency automatically, and every top-level
widget carries it too. If you build a UI without a
bs_page() wrapper — for example inside a larger app shell —
add the dependency once with use_bootstrict():