glassSelect() is an animated single-select dropdown for
Shiny.
For filters that accept several values, see the multi-select guide.
It behaves like a standard Shiny input:
input$<inputId> stores the selected value as a single
character string, or NULL when nothing is selected.
It supports:
glass_select_theme()check_styleupdateGlassSelect()closeGlassSelect(),
input$<inputId>_open, and
input$<inputId>_ready (TRUE once the widget has
initialized)library(shiny)
library(glasstabs)
fruits <- c(
Apple = "apple",
Banana = "banana",
Cherry = "cherry",
Mango = "mango"
)
ui <- fluidPage(
useGlassTabs(),
glassSelect("pick", fruits),
verbatimTextOutput("out")
)
server <- function(input, output, session) {
output$out <- renderPrint(input$pick)
}
if (interactive()) shinyApp(ui, server)# No selection (default)
glassSelect("f", fruits)
# Pre-select one value
glassSelect("f", fruits, selected = "banana")An empty string is a valid choice value, distinct from nothing
selected:
glassSelect("status", c("All statuses" = "", "Active" = "active"), selected = "")
starts on “All statuses” and reports "", while clearing the
field reports NULL. The same holds for
updateGlassSelect(..., selected = "").
glassSelect() supports three selection marker
styles:
"checkbox" (default)"check-only""filled"By default the trigger and dropdown use the signature rounded glass
corners. Pass shape = "square" for crisp, selectize-style
corners so the widget sits flush next to native
selectizeInput() controls without looking out of place:
# Rounded (default)
glassSelect("region_rounded", fruits, shape = "rounded")
# Square - matches selectizeInput()
glassSelect("region_square", fruits, shape = "square")The shape can also be switched at runtime with
updateGlassSelect(session, "region_square", shape = "square").
Here is the complete side-by-side layout with a native Shiny
control:
For parity with native selectizeInput(),
glassSelect() accepts a width, grouped choices
as a named list, and disabled state:
The open dropdown follows the trigger width. Longer labels wrap within that space rather than making a short field unexpectedly wider.
# Fixed / fluid width
glassSelect("region_w", fruits, width = "240px")
# Grouped choices (selectInput()-style named list)
glassSelect("food", list(
Fruit = c(Apple = "apple", Banana = "banana"),
Veg = c(Carrot = "carrot", Pea = "pea")
))
# Disable the whole widget, or specific options
glassSelect("region_d", fruits, disabled = TRUE)
glassSelect("region_dc", fruits, disabled_choices = "banana")disabled and disabled_choices can also be
toggled at runtime with updateGlassSelect().
The default overflow_tooltip = "auto" adds a label
preview only when the selected trigger text is actually shortened with
an ellipsis. It appears on pointer hover or keyboard focus and is capped
for exceptionally large labels; the complete accessible name remains
available. Touch users can open the dropdown to read the label wrapped
inside its matching-width panel. Truncated options in the open dropdown
also reveal a preview on hover or keyboard navigation.
long_industries <- c(
"Renewable energy generation, storage, and grid-scale distribution networks" = "energy",
Healthcare = "healthcare"
)
glassSelect(
"industry",
long_industries,
selected = "energy",
width = "240px",
overflow_tooltip = "auto"
)Use overflow_tooltip = "never" when the visual
explanation is unnecessary. The complete accessible label remains on the
trigger in either mode.
For dynamic layouts, modals, sidebars, or tab changes, you can explicitly close an open dropdown from the server:
The widget also reports whether its dropdown is open:
closeAllGlassSelects(session) closes every open
glasstabs select dropdown in the current session.
Search is enabled by default. Use searchable = "auto"
when short lists should stay simple and search should appear only after
the choices reach a threshold. You can also enable a clear control.
The Clear button is keyboard-reachable too: with the dropdown open, Arrow Down past the last option moves focus to it (Arrow Up returns to the options), and Enter or Space clears the selection. Tab and Escape close the panel as usual.
For example, five categories do not need a search field when the threshold is six:
glassSelect(
"category",
c(Alpha = "alpha", Beta = "beta", Gamma = "gamma", Delta = "delta", Epsilon = "epsilon"),
searchable = "auto",
search_threshold = 6
)The same settings can change at runtime:
Field and motion styling can be adjusted without changing the input value:
glassSelect(
"f",
fruits,
theme = "auto",
dark_selector = "body.app-dark",
tongue = FALSE,
label_bold = TRUE,
label_size = "14px",
search_focus_ring = FALSE
)tongue = TRUE uses the attached glass unfurl. With
FALSE, the panel is detached and uses a short,
native-feeling fade. The quiet search focus style is the default; set
search_focus_ring = TRUE for the accent outline. When an
app uses a custom dark-mode class instead of Bootstrap’s
data-bs-theme, pass that class through
dark_selector while using theme = "auto".
To always hide the search box:
For hundreds or thousands of choices, set server = TRUE
in the UI and register glassSelectServer() in the server
function. The browser renders only an initial slice of choices and asks
Shiny for matching choices as the user types.
many_choices <- stats::setNames(
sprintf("value-%04d", 1:2000),
sprintf("Choice %04d", 1:2000)
)
ui <- fluidPage(
useGlassTabs(),
glassSelect(
"pick",
many_choices,
selected = "value-1500",
clearable = TRUE,
server = TRUE,
server_limit = 30
),
verbatimTextOutput("out")
)
server <- function(input, output, session) {
glassSelectServer("pick", many_choices, session = session, limit = 30)
output$out <- renderPrint(input$pick)
}
if (interactive()) shinyApp(ui, server)This pattern also works when the control is created dynamically with
renderUI(). Keep the glassSelectServer() call
in the server function for the same inputId.
The two limits are independent — see the multi-select guide’s
server-search section for the full table: server_limit
bounds the initial slice, while the helper’s limit bounds
each search’s matches. Results are bounded, not paginated: each search
returns the first N matches, with no next-page cursor. And as with the
multi-select, server = TRUE with search disabled is
unsupported: without a search input the user cannot reach choices
outside the initial slice. The explicit “All” option works in server
mode (see below); it sits first in the initial slice and is restored
across search responses.
You can prepend an explicit “All” option:
glassSelect(
"f",
fruits,
include_all = TRUE,
all_choice_label = "All fruits",
all_choice_value = "__all__"
)This is useful when you want the UI to distinguish between: - no selection - a deliberate “All” selection
The option is off by default (include_all = FALSE). When
on, the widget prepends one row with label all_choice_label
(default "All categories") and value
all_choice_value (default "__all__"); choosing
it returns that value, not the full choice list, so the app decides what
“All” means downstream. The row is a client-side affordance, not a data
row: it survives choices updates (the row and an active “All” selection
are both preserved across updateGlassSelect() calls) and it
works in server mode too, where it sits first in the initial slice and
is restored across search responses. The search threshold counts real
choices only — the All row is excluded from it.
Read the current value directly from
input$<inputId>:
If you want a small convenience wrapper, use
glassSelectValue():
updateGlassSelect() can update: - available choices -
current selection - selection marker style
It follows Shiny-style update semantics:
choices = NULL leaves choices unchangedselected = NULL leaves selection unchangedselected = character(0) clears the selectionui <- fluidPage(
useGlassTabs(),
actionButton("subset", "Keep first 2 fruits"),
actionButton("banana", "Select banana"),
actionButton("clear", "Clear"),
actionButton("filled", "Use filled style"),
glassSelect("pick", fruits, clearable = TRUE),
verbatimTextOutput("out")
)
server <- function(input, output, session) {
output$out <- renderPrint(input$pick)
observeEvent(input$subset, {
updateGlassSelect(
session,
"pick",
choices = fruits[1:2]
)
})
observeEvent(input$banana, {
updateGlassSelect(
session,
"pick",
selected = "banana"
)
})
observeEvent(input$clear, {
updateGlassSelect(
session,
"pick",
selected = character(0)
)
})
}
if (interactive()) shinyApp(ui, server)When choices is updated without selected,
the widget keeps the current value if it still exists in the new set of
choices. If it no longer exists, the selection clears and the value
becomes NULL — there is no stale-value state, and no
separate “invalid” marker. To keep a selection that is temporarily
absent instead, do not remove it from the choice set; the multi-select
updater’s drop_invalid = FALSE has no single-select
equivalent. Two more update rules: passing more than one value to
selected is an error (use the multi-select updater for
multiple values), and the label cannot be updated — there is no
label argument on updateGlassSelect(), so a
label change means recreating the widget with
renderUI().
glassSelect("f", fruits, theme = "dark")
glassSelect("f", fruits, theme = "light")
glassSelect("f", fruits, theme = "auto")Use theme = "auto" inside Bootstrap 5 / bslib apps to
follow data-bs-theme light and dark modes without server
code.
glass_select_theme()glassSelect(
"f",
fruits,
theme = glass_select_theme(
mode = "dark",
accent_color = "#f59e0b"
)
)
glassSelect(
"f",
fruits,
theme = glass_select_theme(
bg_color = "#1a0a2e",
border_color = "#a855f7",
text_color = "#ede9fe",
accent_color = "#a855f7"
)
)
#other themes
glassSelect(
"f",
fruits,
theme = glass_select_theme(
mode = "light",
accent_color = "#2563eb"
)
)
#more themes
glassSelect(
"f",
fruits,
theme = glass_select_theme(
mode = "light",
bg_color = "#ffffff",
border_color = "rgba(0,0,0,0.15)",
text_color = "#111111",
accent_color = "#111111",
label_color = "#111111"
)
)glassSelect() can be used without the tab widget. You
only need useGlassTabs() to load the package CSS and
JavaScript assets: