Form inputs are where bootstrict stays closest to Shiny.
Most inputs delegate to the matching
shiny::*Input() and only re-dress the markup as
Bootstrap 5 — so input$id and Shiny’s own
updateXxx() work exactly as you already know. Two native
inputs have no Shiny equivalent and get their own
update_bs_*() helpers.
The two families
| Family | Constructors | Reactive value | Update from server |
|---|---|---|---|
| Delegating (wrap Shiny) | text, textarea, numeric, password, select, checkbox, switch, radio, checkbox-group, file | input$id |
Shiny’s updateXxx()
|
| Native (own binding) |
bs_range_input(), bs_color_input(),
bs_date_input(), bs_date_range_input(),
bs_radio_button_input(),
bs_checkbox_button_input()
|
input$id |
update_bs_range(), update_bs_color(),
update_bs_date_input(),
update_bs_toggle_buttons()
|
Every input takes a leading id, an optional
label, and — for delegating inputs — a help
string that renders as .form-text below the control, plus a
width. Where Bootstrap offers sizing,
size = "sm" or "lg" is available.
Text-like inputs
bs_text_input("name", "Your name", placeholder = "Jane Doe")
bs_textarea_input("bio", "Bio", rows = 4)
bs_numeric_input("age", "Age", value = 30, min = 0, max = 120)
bs_password_input("pw", "Password", help = "Must be 8–20 characters.")Signatures share a common shape; bs_text_input() is
representative:
bs_text_input(
id,
label = NULL,
value = "",
...,
placeholder = NULL,
size = NULL, # "sm" or "lg"
help = NULL, # .form-text below the control
width = NULL
)Because these delegate to Shiny, update them with Shiny’s own updaters:
updateTextInput(session, "name", value = "Colin")Select
bs_select_input() renders a plain Bootstrap
<select class="form-select">. Note this
differs from Shiny’s selectInput(): selectize is off, so
there is no search / tagging box.
bs_select_input("fruit", "Fruit", c("Apple", "Pear"))
bs_select_input(
"sort_by", "Sort by",
c("Magnitude" = "mag", "Depth" = "depth"),
selected = "mag"
)multiple = TRUE renders a multi-select listbox.
Checkboxes, switches, radios and groups
A single checkbox, and its switch variant:
bs_checkbox_input("agree", "I agree", value = TRUE)
bs_checkbox_input("dark", "Dark mode", switch = TRUE)
bs_switch_input("dark", "Dark mode") # convenience for switch = TRUERadio buttons and checkbox groups take choices (named or
unnamed) and an optional selected.
inline = TRUE lays the choices out horizontally:
bs_radio_input("size", "Size", c("S", "M", "L"), selected = "M")
bs_checkbox_group_input("opts", "Options", c("A", "B", "C"), selected = "A", inline = TRUE)All of the check-style inputs support reverse = TRUE,
which puts the label before the control
(.form-check-reverse).
Range and colour (native inputs)
These two have no Shiny equivalent, so they ship their own bindings.
They still report input$id, but you drive them with the
bootstrict helpers — Shiny’s
updateSliderInput() will not reach them.
bs_range_input() is a native
<input type="range"> (not Shiny’s
sliderInput() — no ticks, animation or ion.rangeSlider
features):
bs_range_input("vol", "Volume", value = 50, min = 0, max = 100, step = 1)If value = NULL, the browser starts it at the midpoint
(min + max) / 2. Update it from the server with
update_bs_range(id, value):
update_bs_range("vol", 75)bs_color_input() is a native colour picker;
value must be a 6-digit hex string:
bs_color_input("col", "Pick a colour", value = "#0d6efd")
update_bs_color("col", "#198754")Dates and files
bs_date_input("day", "Pick a day", value = "2026-06-26")
bs_date_range_input("range", "Period", start = "2026-01-01", end = "2026-12-31")
bs_file_input("upload", "Upload a file", accept = ".csv")These delegate to shiny::dateInput(),
dateRangeInput() and fileInput() respectively,
so update them with the matching Shiny updater.
Composing inputs
Three layout helpers restyle or group inputs without touching their bindings.
Input groups
bs_input_group() places add-ons and controls on a single
line. When you pass a full bs_*_input(), only its bare
control is kept (its label and help text are dropped, since the group is
one line):
bs_input_group(
bs_input_group_text("@"),
bs_text_input("user", placeholder = "Username")
)
bs_input_group(
bs_input_group_text("$"),
bs_numeric_input("amount", value = 0),
bs_input_group_text(".00"),
size = "lg"
)A handful of inputs whose binding lives on their container —
bs_date_input(), bs_date_range_input(),
bs_file_input(), bs_radio_input() and
bs_checkbox_group_input() — cannot be unwrapped into a
group and will raise a clear error if you try.
Floating labels
bs_floating_label() reshapes an input into the Bootstrap
5 floating-label style. It moves the existing control
rather than rebuilding it, so the binding is preserved. If you do not
pass a label, the control’s existing label text is
reused:
bs_floating_label(bs_text_input("email", "Email address"))
bs_floating_label(bs_select_input("fruit", "Fruit", c("Apple", "Pear")))The same container-bound inputs listed above cannot be floated.
Forms, labels, help and validation
bs_form() is a plain <form> wrapper
for grouping controls; novalidate = TRUE disables the
browser’s native validation UI:
bs_form(
bs_text_input("email", "Email"),
bs_button("submit", "Submit", color = "primary")
)The lower-level pieces are available when you hand-build a control:
-
bs_form_label("email", "Email address")— a.form-labeltied to a control by itsforattribute. -
bs_form_text("Must be 8–20 characters.")— muted helper text (the same thing thehelp=argument produces). -
bs_valid_feedback("Looks good!")andbs_invalid_feedback("Please choose a username.")— inline validation messages that Bootstrap shows when the control is marked valid/invalid.
Dates
bs_date_input() is a native
<input type="date"> with the
.form-control class, which is what the Bootstrap 5.3 forms
page shows: the browser supplies the calendar.
It deliberately does not delegate to shiny::dateInput(),
which loads bootstrap-datepicker — a third-party stylesheet
whose calendar markup (.datepicker,
.datepicker-days, …) appears nowhere in the Bootstrap
documentation and which a designer’s SASS sheet cannot reach. The cost
is real: format, language,
weekstart and datesdisabled are gone, because
the browser owns the presentation and Bootstrap offers no way to ask it
for something else.
bs_date_input("day", "Day", value = "2026-06-26", min = "2026-01-01")
bs_date_range_input("period", "Period", start = "2026-01-01")input$day is a Date, and NA
while the field is empty. A range is a length-2 Date, so it
keeps both positions when only one end is filled in. Drive them with
update_bs_date_input() and
update_bs_date_range_input() —
shiny::updateDateInput() will not reach them. Pass
NA to clear a field, and NULL (the default) to
leave it alone.
update_bs_date_input("day", value = Sys.Date())
update_bs_date_range_input("period", start = "2026-02-01", end = NA)Bootstrap has no date-range component, so
bs_date_range_input() is the assembly its docs prescribe:
two fields in an .input-group with an
.input-group-text between them. Each field carries its own
aria-label, since one label cannot name both.
Toggle buttons
Bootstrap’s segmented control is a row of buttons backed by hidden
.btn-check inputs. Its markup is not the
.form-check one with a different skin – the input is a
sibling of its label – so these are native controls with their
own constructors, driven by update_bs_toggle_buttons()
rather than shiny::updateRadioButtons().
bs_radio_button_input("size", "Size", c(Small = "s", Large = "l"))
bs_checkbox_button_input("opts", "Options", c("a", "b"))input$size is a single string; input$opts
is a character vector, empty when nothing is picked.
outline = FALSE, color, size and
vertical cover the Bootstrap variants.
Validation
Bootstrap only displays a feedback message when it is a following
sibling of the control carrying .is-valid /
.is-invalid. A message placed after a
bs_*_input() is a sibling of shiny’s input container, not
of the control inside it, so it would never appear.
bs_feedback() puts it in the right place:
bs_feedback(
bs_text_input("user", "Username"),
invalid = "Please choose a username."
)Declare the messages in the UI, then switch the state from the server
with set_bs_validation():
observeEvent(input$submit, {
if (input$user %in% taken) {
set_bs_validation("user", "invalid", "That name is taken.")
} else {
set_bs_validation("user", "valid")
}
})set_bs_validation(id, "none") clears the state. Pass
state = to bs_feedback() when a control should
render already marked.
The bare bs_valid_feedback() /
bs_invalid_feedback() divs remain useful in a hand-built
<form>, where you control the sibling order
yourself.
Putting it together
A small settings form, driven from the server:
ui <- bs_page(
bs_container(
class = "py-4",
bs_form(
bs_floating_label(bs_text_input("email", "Email address")),
bs_range_input("vol", "Volume", value = 50),
bs_switch_input("notify", "Email me updates", value = TRUE),
bs_button("save", "Save settings", color = "primary")
)
)
)
server <- function(input, output, session) {
observeEvent(input$save, {
str(list(email = input$email, vol = input$vol, notify = input$notify))
update_bs_range("vol", 50) # native input → bootstrict helper
updateTextInput(session, "email", value = "") # delegating input → Shiny
})
}
shinyApp(ui, server)