This article covers the overlay widgets — modal, offcanvas, toast —
plus tooltips, popovers and scrollspy, and pulls together the one
pattern that runs through the whole package: interactive widgets report
their state as input$id and are controlled from the server
with id-first helpers.
The overlay model: declare in the UI, open by id
This is the biggest deliberate difference from Shiny. Shiny builds
overlays on the server (showModal(modalDialog(...))).
bootstrict follows the native Bootstrap pattern:
declare the overlay once in the UI with an id, then
open or close it by id from the server.
Two consequences follow:
-
Place overlays at the top level of the page — a
direct child of
bs_page()/bs_container(), not nested inside abs_card()or other positioned element, which Bootstrap can clip or mis-position. -
Every overlay reports its open state as
input$id(TRUEwhen shown) — something Shiny modals do not do.
Modal
Declare a bs_modal() with an id; open it
with show_bs_modal().
ui <- bs_page(
bs_container(
bs_button("open", "Open modal", color = "primary")
),
# top level of the page, not inside the container's card
bs_modal(
"info",
"Modal body text.",
title = "Heads up",
footer = bs_button("ok", "OK", color = "primary")
)
)
server <- function(input, output, session) {
observeEvent(input$open, show_bs_modal("info"))
observeEvent(input$info, message("modal open state: ", input$info))
}
shinyApp(ui, server)The server helpers are show_bs_modal(),
hide_bs_modal() and toggle_bs_modal() — each
id-first, session-last:
show_bs_modal("info")
hide_bs_modal("info")Options: size
("sm"/"lg"/"xl"),
centered, scrollable, fullscreen
(TRUE, or a breakpoint for fullscreen-below),
backdrop (TRUE / "static" /
FALSE) and keyboard (Escape to close). The
pieces bs_modal_header(), bs_modal_title(),
bs_modal_body() and bs_modal_footer() are
there when you want to hand-build the dialog.
Dynamic content
An overlay is declared once, but its content need not be fixed: put a
uiOutput() in the body and render into it. This is how you
show a record the user just picked, without rebuilding the modal.
ui <- bs_page(
bs_container(
bs_button("show", "Show details"),
bs_modal("details", uiOutput("details_body"), title = "Details")
)
)
server <- function(input, output, session) {
output$details_body <- renderUI({
bs_table(head(mtcars, input$show %% 5 + 1))
})
observeEvent(input$show, show_bs_modal("details"))
}Re-rendering while the overlay is open is safe: the widget is torn down after its hide transition, so the page is never left scroll-locked.
Opening without a server round trip
bs_modal_trigger() opens a modal declaratively from the
UI — no observeEvent needed:
bs_modal_trigger("info", "Open modal", color = "primary")Offcanvas (drawer)
An offcanvas is a panel that slides in from an edge — Shiny has no equivalent. Same model as the modal: declare it, open it by id.
ui <- bs_page(
bs_container(
bs_offcanvas_trigger("filters", "Filters", class = "btn-primary")
),
bs_offcanvas(
"filters",
title = "Filter events",
placement = "end", # "start"/"end"/"top"/"bottom"
bs_range_input("mag", "Minimum magnitude", value = 4, min = 4, max = 6.5),
bs_button("reset", "Reset", color = "secondary", outline = TRUE)
)
)
server <- function(input, output, session) {
observeEvent(input$reset, {
update_bs_range("mag", 4)
hide_bs_offcanvas("filters")
})
}
shinyApp(ui, server)Server helpers: show_bs_offcanvas(),
hide_bs_offcanvas(), toggle_bs_offcanvas(),
plus the UI bs_offcanvas_trigger(). backdrop
behaves as for the modal; responsive = "lg" shows the panel
inline from that breakpoint up and only turns it into a drawer below
it.
Toasts
A toast is a small, transient notification. There are two ways to use them.
Declared toast. Declare a bs_toast()
(usually inside a bs_toast_container() that positions it)
and show it by id:
ui <- bs_page(
bs_button("save", "Save"),
bs_toast_container(
bs_toast("saved", "Your changes were saved.", title = "Done"),
placement = "top-end"
)
)
server <- function(input, output, session) {
observeEvent(input$save, show_bs_toast("saved"))
}Notification toast. bs_notify_toast()
is the one server-built widget — the closest thing to
shiny::showNotification(). It builds and shows a
transient toast in one call, creating a container on demand and cleaning
itself up afterwards. Note the argument order: body first,
session last.
bs_notify_toast(
"Showing the full catalogue again.",
title = "Filters reset",
color = "success"
)body and title are plain text only.
color is a theme colour (.text-bg-*),
autohide/delay control the auto-dismiss, and
placement uses the same nine keywords as
bs_toast_container() ("top-end",
"bottom-center", …).
Tooltips and popovers
These decorate a tag you already have — they are
pipe-friendly wrappers, not constructors. Bootstrap does not
auto-initialise them, but bootstrict does. They are
UI-only: there is no server-side toggle.
bs_tooltip(bs_button(label = "Hover me"), "Tooltip text", placement = "top")
bs_popover(
bs_button(label = "Click me"),
"Popover body content",
title = "Heads up",
placement = "right"
)bs_tooltip() takes title;
bs_popover() takes content (and an optional
title). Both accept placement
("top"/"right"/"bottom"/"left"),
trigger and html = TRUE to allow HTML
content.
Scrollspy
bs_scrollspy() wraps a scrollable region and updates a
nav (identified by target) as the user scrolls. It needs an
id to initialise (one is auto-generated if you omit it),
including inside renderUI(); the active nav link’s href is
reported as input$id.
bs_scrollspy(
"section-nav",
tags$h4("Section one"), tags$p("..."),
tags$h4("Section two"), tags$p("..."),
offset = 100
)Inside a Shiny module
The two halves are not symmetric, and it is the one place bootstrict asks you to think.
Server helpers namespace for you.
show_bs_modal("info") inside a moduleServer()
resolves to the module’s info, because the helper reads the
session’s namespace. Never wrap them in ns().
UI triggers do not. bs_modal_trigger(),
bs_offcanvas_trigger() and
bs_collapse_trigger() take the id of a target
element, which is a plain string at UI build time with no session
to consult. Wrap those in ns() yourself, exactly as you
would a tabPanel id.
panel_ui <- function(id) {
ns <- NS(id)
bs_container(
# The widget's own id: namespaced, like any input.
bs_modal(ns("info"), "Body", title = "Heads up"),
# A trigger points at that element, so it needs the same treatment.
bs_modal_trigger(ns("info"), "Open"),
bs_button(ns("open"), "Open from the server")
)
}
panel_server <- function(id) {
moduleServer(id, function(input, output, session) {
# No ns() here: the helper takes it from the session.
observeEvent(input$open, show_bs_modal("info"))
})
}The same rule covers bs_collapse_trigger() pointing at a
bs_collapse(), and bs_scrollspy(target =)
pointing at a nav.
The pattern, in one place
Every interactive widget in bootstrict follows the same
two-way contract:
| Widget | Reads back as input$id
|
Server control |
|---|---|---|
bs_tabset() |
active panel value | update_bs_tabset() |
bs_accordion() |
open panel value(s) | update_bs_accordion() |
bs_carousel() |
active slide index (0-based) | update_bs_carousel() |
bs_collapse() |
shown/hidden (TRUE/FALSE) |
update_bs_collapse() |
bs_list_group(id=) |
selected item value | update_bs_list_group() |
bs_progress_bar(id=) |
(display only) | update_bs_progress() |
bs_range_input() |
value | update_bs_range() |
bs_color_input() |
value | update_bs_color() |
bs_modal() |
open (TRUE/FALSE) |
show_/hide_/toggle_bs_modal() |
bs_offcanvas() |
open (TRUE/FALSE) |
show_/hide_/toggle_bs_offcanvas() |
bs_toast() |
visible (TRUE/FALSE) |
show_/hide_bs_toast() |
| page colour mode | — | set_bs_color_mode() |
Every one of these helpers takes the id
first and the session last and
optional (it defaults to the current reactive domain), and ids
are namespaced automatically inside Shiny modules — so the same call
works at the top level and inside a module with no changes:
update_bs_tabset("tabs", selected = "profile") # no session argument needed
show_bs_modal("info")
set_bs_color_mode("dark")