bootstrict re-implements the Bootstrap
5.3 layout, content, forms and component library as Shiny UI
functions. Every widget mirrors the Bootstrap 5.3 HTML structure
one-to-one, so a designer’s mockup and exported SASS
variables drop straight into a Shiny application — with minimum
deviation from Shiny itself.
Why bootstrict
Working with a designer who does not know Shiny is awkward for two reasons:
- some Shiny components are not plain Bootstrap; and
- some Bootstrap components are missing from Shiny.
bootstrict closes that gap by giving you the whole
Bootstrap 5 surface, and nothing more — so you can tell
a designer: “use anything from the Bootstrap 5.3 docs,
but nothing else.” Bootstrap 5.3 is vendored by the package itself
and compiled with sass, so the
version reaching the browser is the one bootstrap_version()
reports.
A first app
Every app starts with a page constructor (bs_page()), a
theme, and Bootstrap widgets composed like any other Shiny UI:
ui <- bs_page(
title = "Sign in",
theme = bootstrict_theme(primary = "#ff6600"),
bs_container(
bs_card(
bs_card_header("Sign in"),
bs_card_body(
bs_text_input("email", "Email", placeholder = "you@example.com"),
bs_password_input("pw", "Password"),
bs_button("go", "Sign in", color = "primary")
)
)
)
)
server <- function(input, output, session) {
observeEvent(input$go, {
print(input$email)
print(input$pw)
})
}
shinyApp(ui, server)Conventions (minimum deviation from Shiny)
Once you know these four rules you know how every constructor in the package behaves:
-
Naming. Every constructor is
snake_case, prefixedbs_(bs_card(),bs_text_input(), …). Nothing masks a Shiny function. -
...works exactly like Shiny/htmltools. Named arguments become HTML attributes; unnamed arguments become children. Extraclassvalues passed through...are merged with the component’s own classes. -
Interactive widgets take a leading
id. Their value is then available asinput$id, just like a Shiny input. -
Form inputs delegate to the matching
shiny::*Input(). So the reactive value and everyupdateXxx()keep working identically —bootstrictonly layers Bootstrap 5 markup, sizing, help text, switches, input groups and floating labels on top.
The map of the package
bootstrict covers the whole Bootstrap 5.3 catalogue.
Each area has its own article:
| Article | What it covers |
|---|---|
| Theming and the designer hand-off |
bootstrict_theme(), SASS variable sheets, colour
modes |
| Layout and the grid | pages, containers, the 12-column grid, stacks |
| Content: tables, media and typography |
bs_table(), images, figures, blockquotes, headings,
lists |
| Forms and inputs | every input, input groups, floating labels, validation |
| Components | accordion, alert, badge, buttons, card, carousel, collapse, list group, progress, spinner, placeholder |
| Navigation | nav & tabs, navbar, breadcrumb, pagination, dropdown |
| Overlays and server-driven interactivity | modal, offcanvas, toast, tooltips/popovers, the
update_bs_*() pattern |
How bootstrict differs from Shiny
bootstrict stays as close to Shiny as it can, but a
handful of behaviours differ on purpose, to follow
native Bootstrap. If you already know Shiny, these are the things to
watch for — each is covered in depth in the article listed.
Overlays live in the UI — they are not built from the server
Shiny builds modals and notifications on the server
(showModal(modalDialog(...))). bootstrict
follows the native Bootstrap pattern: the modal, toast or offcanvas is
declared once in the UI with an id, and
the server only opens or closes it by id.
| Task | Shiny | bootstrict |
|---|---|---|
| Open a modal | showModal(modalDialog(...)) |
declare bs_modal("id", …), then
show_bs_modal("id")
|
| Close a modal | removeModal() |
hide_bs_modal("id") |
| Notification | showNotification("…") |
bs_notify_toast("…") |
| Offcanvas / drawer | (not in Shiny) | declare bs_offcanvas("id", …), then
show_bs_offcanvas("id")
|
Place overlay widgets at the top level of the page
(a direct child of bs_page() /
bs_container()), never nested inside a
bs_card() — Bootstrap can otherwise clip or mis-position
them. See Overlays.
Server helpers take id first and session
last
Shiny’s updaters take the session first:
updateTextInput(session, "id", …). Every
bootstrict helper takes the id first and
the session last and optional (it defaults to the
current reactive domain), and ids are namespaced automatically inside
modules:
update_bs_tabset("tabs", selected = "profile") # no session argument needed
show_bs_modal("info")Two kinds of inputs
-
Inputs that delegate to Shiny —
bs_text_input(),bs_numeric_input(),bs_select_input(),bs_radio_input(),bs_checkbox_input(),bs_checkbox_group_input(),bs_file_input(),bs_textarea_input(),bs_password_input(). They wrap the matchingshiny::*Input(), soinput$idand Shiny’s ownupdateXxx()keep working unchanged. -
Native inputs —
bs_range_input(),bs_color_input(),bs_date_input(),bs_date_range_input()and the.btn-checksegmented controls (bs_radio_button_input(),bs_checkbox_button_input()). Either Shiny has no equivalent, or its equivalent is not Bootstrap markup. They ship their own bindings, so drive them withupdate_bs_range()/update_bs_color()/update_bs_date_input()/update_bs_toggle_buttons().
bs_button() is an action button only when given an
id
bs_button("go", "Go") behaves exactly like
shiny::actionButton() — input$go is the click
count. Called without an id it is an
inert, styled button; reactivity is opt-in.
Run the bundled apps
# The catalogue: every widget, one card each.
shiny::runApp(system.file("examples/demo", package = "bootstrict"))
# The showcase: Quake Watch, a seismic monitor built on datasets::quakes.
shiny::runApp(system.file("examples/quakewatch", package = "bootstrict"))