Skip to content

Repository files navigation

bootstrict

Strict, faithful Bootstrap 5.3 widgets for Shiny — with minimum deviation from Shiny itself.

bootstrict re-implements the Bootstrap 5.3 layout, content, forms and component library as Shiny UI functions.

Why

Working with an external designer that doesn't know {shiny} can be complex because of two things:

  • some Shiny components are not plain Bootstrap;
  • some Bootstrap components are missing from Shiny.

bootstrict tries to fix this gap by giving you the whole Bootstrap 5 surface, and nothing more, meaning that you can tell a designer: "you can use anything from Bootstrap 5.3. But nothing more".

Installation

# install.packages("pak")
pak::pak("thinkr-open/bootstrict")

Bootstrap 5.3 is vendored by bootstrict itself and compiled with sass, so the version reaching the browser is fixed by this package rather than by whichever theming package happens to be installed — bootstrap_version() reports it. The markup bootstrict emits matches the runtime it ships.

The designer hand-off

Every widget mirrors the Bootstrap 5.3 HTML structure one-to-one, so a designer's mockup (for example in Figma) and exported SASS variables drop straight into a Shiny app. Interactive components report their state to the server and can be driven from the server with update_*() helpers.

One thing falls short of that, inherited from the Shiny inputs the package delegates to: every delegated input keeps Shiny's div.form-group.shiny-input-container wrapper. Everything else is the reference markup, and the test suite snapshots it — no third-party widget library is shipped, and a test enforces that.

The rule, when the two collide: if it is not in the Bootstrap documentation it is not in bootstrict, even where that loses a Shiny feature.

The motivating workflow: a designer works in Figma, stays strictly within the Bootstrap 5.3 docs, and exports a _variables.scss sheet.

You received a Figma mockup and the variables, and can implement this directly into shiny.

library(shiny)
library(bootstrict)


variables <- tempfile(fileext = ".scss")
writeLines(
  c(
    "$primary: #ff6600;",
    "$border-radius:5rem;"
  ),
  variables
)

ui <- bs_page(
  theme = bootstrict_theme(
    variables = variables
  ),
  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")
      )
    ),
    # Declare the modal once, at the top level of the page (not inside the
    # card): Bootstrap can clip or mis-position overlays nested in another
    # element. The server then opens it by id (see below).
    bs_modal(
      "info",
      "Modal body text.",
      title = "Heads up"
    )
  )
)

server <- function(input, output, session) {
  observeEvent(input$go, {
    print(input$email)
    print(input$pw)
    show_bs_modal("info")
   })
}

shinyApp(ui, server)

bootstrict_theme() collects SASS variable overrides for the vendored Bootstrap tree; parse_scss_variables() turns a $name: value; sheet into the named list it takes. Inline overrides win over the file:

bootstrict_theme(
  variables = "_variables.scss",
  primary = "#ff6600"
)

Conventions (minimum deviation from Shiny)

  • Every constructor is snake_case, prefixed bs_ (no masking of Shiny).
  • ... works exactly like Shiny/htmltools: named args become HTML attributes, unnamed args become children.
  • Interactive widgets take a leading id; their value is input$id.
  • 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.

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.

Overlay widgets live in the UI — they aren't built from the server

Shiny builds modals and notifications on the server (showModal(modalDialog(...)), showNotification(...)). bootstrict follows the native Bootstrap pattern instead: the modal, toast or offcanvas is declared once in the UI with an id, and the server only opens or closes it by id.

ui <- bs_page(
  bs_button(
    "open",
    "Open"
  ),
  # declared in the UI
  bs_modal(
    "info",
    "Body text",
    title = "Heads up"
  )
)
server <- function(input, output, session) {
  observeEvent(
    input$open, {
      # opened by id
      show_bs_modal("info")
    }
   )
}
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("…") — the one server-built widget (builds + shows a transient toast)
Offcanvas / drawer (not in Shiny) declare bs_offcanvas("id", …), then show_bs_offcanvas("id")

Two consequences:

  • Place overlay widgets at the top level of the page (a direct child of bs_page() / bs_container()), not nested inside a bs_card() or other positioned element — Bootstrap can otherwise clip or mis-position them.
  • Every overlay reports its open state back as input$id (TRUE when shown) — Shiny modals don't. You can also open them with no server round trip using the UI triggers bs_modal_trigger(), bs_offcanvas_trigger() and bs_collapse_trigger().

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() and only restyle the markup, so input$id and Shiny's own updateXxx() keep working unchanged — use shiny::updateTextInput() etc. for these.
  2. Native inputsbs_range_input(), bs_color_input(), bs_date_input(), bs_date_range_input(), bs_radio_button_input() and 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() (Shiny's updateSliderInput() and updateDateInput() won't reach them).

A few specifics worth knowing:

  • bs_select_input() renders a plain Bootstrap <select> — selectize is off, so there is no search / tagging box that Shiny's selectInput() adds by default.
  • bs_range_input() is a native HTML <input type="range">, not Shiny's sliderInput() (no ticks, animation or ion.rangeSlider features).
  • bs_file_input() is the Bootstrap 5.3 <input class="form-control" type="file">, so the browser draws the button and the file name — not Shiny's "Browse" button beside a readonly text box, which is Bootstrap 3 markup.
  • bs_date_input() / bs_date_range_input() are native <input type="date"> fields, so the browser supplies the calendar. They do not delegate to Shiny, which would pull in bootstrap-datepicker — a third-party stylesheet whose calendar is nowhere in the Bootstrap docs. The cost is that format, language, weekstart and datesdisabled are gone; drive them with update_bs_date_input().
  • Validation feedback needs bs_feedback(): Bootstrap only shows a message that is a sibling of the marked control, and a bare bs_invalid_feedback() after an input is a sibling of Shiny's wrapper. set_bs_validation() switches the state from the server.

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.

Interactive components are driven by update_bs_*()

Give a widget an id and it reports its state as input$id and takes instructions from a matching helper:

Widget input$id Server helper
bs_accordion() open panel value(s) update_bs_accordion()
bs_tabset() active tab update_bs_tabset()
bs_carousel() active slide update_bs_carousel()
bs_collapse() open / closed update_bs_collapse()
bs_list_group() selected item update_bs_list_group()
bs_nav(), bs_navbar_nav() active link update_bs_nav()
bs_pagination() active page update_bs_pagination()
bs_dropdown(), bs_nav_dropdown() open / closed show_bs_dropdown()
bs_alert() still on the page close_bs_alert()
bs_progress() update_bs_progress()

Tabs in particular use bs_tabset() + bs_tab_panel() (an id is required and panels are validated) — not tabsetPanel() / tabPanel(). For the other widgets the id is optional: without one they are static markup, which is what a decorative alert or a nav of plain links should be.

Tooltips & popovers decorate an existing tag

bs_tooltip(tag, "text") and bs_popover(tag, "content") wrap a tag you already have (pipe-friendly) and are initialised client-side by bootstrict (Bootstrap does not auto-initialise them). They are UI-only — there is no server-side update / toggle for them.

Interactivity

Interactive components report state and are controllable from the server:

ui <- bs_page(
  bs_accordion("acc",
    bs_accordion_panel("One", "...", value = "one"),
    bs_accordion_panel("Two", "...", value = "two")),
  bs_button("open_two", "Open panel two")
)

server <- function(input, output, session) {
  observe( print(input$acc) )                         # open panel value(s)
  observeEvent(input$open_two,
               update_bs_accordion("acc", open = "two"))
}

The same pattern covers tabs (input$id = active tab, update_bs_tabset()), the carousel (active slide, update_bs_carousel()), collapse, list-group selection, modals (show_bs_modal() / hide_bs_modal()), offcanvas, toasts (show_bs_toast(), bs_notify_toast()) and progress bars (update_bs_progress()).

Coverage

Layoutbs_container(), bs_row(), bs_col() (responsive spans, offsets, order, gutters, alignment), bs_hstack() / bs_vstack() stacks.

Contentbs_table() (data frame → Bootstrap table), bs_img(), bs_figure(), bs_blockquote(), bs_display_heading(), bs_lead(), lists.

Forms — text / textarea / number / password / select / checkbox / switch / radio / checkbox-group / range / color / file / date / date-range inputs, the .btn-check segmented controls (bs_radio_button_input(), bs_checkbox_button_input()), plus bs_input_group(), bs_floating_label(), bs_form(), working validation feedback (bs_feedback() / set_bs_validation()), and .form-check-reverse via reverse = TRUE.

Components — accordion, alert, badge, breadcrumb, buttons & button groups, card, carousel, close button, collapse, dropdown (standalone and in a nav or navbar, bs_nav_dropdown()), list group, modal, nav & tabs (including .nav-underline, 5.3), navbar, offcanvas, pagination, placeholder, popover, progress (including .progress-stacked, 5.3), spinner, toast, tooltip, scrollspy, plus helpers (bs_ratio(), bs_visually_hidden(), bs_vr(), bs_icon_link()).

Colour modes (5.3) — set the initial mode with bs_page(color_mode = "dark"), or "auto" to follow the operating system; switch it from the server with set_bs_color_mode("light") and read the mode in force as input$bootstrict_color_mode. A mode the user picks is remembered in the browser. Component-level dark = TRUE / theme = "dark" arguments emit data-bs-theme per the 5.3 idiom.

Utilities — none are wrapped, deliberately. Every constructor takes a trailing class and forwards named ... as attributes, so a mockup's class="p-4 text-center" transfers verbatim and a utility Bootstrap adds later works the day it ships.

See ?bootstrict and run the demos:

# The catalogue: every widget, one card each.
shiny::runApp(system.file("examples/demo", package = "bootstrict"))

# The showcase: Quake Watch, a seismic monitor for the Fiji region built on
# datasets::quakes — designer theme from a _variables.scss sheet, offcanvas
# filter drawer, modal event records, colour modes, stacked progress.
shiny::runApp(system.file("examples/quakewatch", package = "bootstrict"))

License

MIT © Colin Fay / ThinkR

About

Strict, faithful Bootstrap 5.3 widgets for Shiny — with minimum deviation from Shiny itself.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages