Skip to contents

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, prefixed bs_ (bs_card(), bs_text_input(), …). Nothing masks a Shiny function.
  • ... works exactly like Shiny/htmltools. Named arguments become HTML attributes; unnamed arguments become children. Extra class values passed through ... are merged with the component’s own classes.
  • Interactive widgets take a leading id. Their value is then available as input$id, just like a Shiny input.
  • Form inputs delegate to the matching shiny::*Input(). So the reactive value and every updateXxx() keep working identically — bootstrict only 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

  1. Inputs that delegate to Shinybs_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 matching shiny::*Input(), so input$id and Shiny’s own updateXxx() keep working unchanged.
  2. Native inputsbs_range_input(), bs_color_input(), bs_date_input(), bs_date_range_input() and the .btn-check segmented 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 with update_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"))