--- title: "Single-Select Dropdown with glasstabs" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Single-Select Dropdown with glasstabs} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} editor_options: markdown: wrap: 72 --- ```{r setup, include=FALSE} knitr::opts_chunk$set(eval = FALSE) ``` ## Overview `glassSelect()` is an animated single-select dropdown for Shiny. For filters that accept several values, see the [multi-select guide](multiselect.html). It behaves like a standard Shiny input: `input$` stores the selected value as a single character string, or `NULL` when nothing is selected. It supports: - optional labels - search and clear controls - built-in dark and light themes - custom theming with `glass_select_theme()` - selectable marker styles via `check_style` - server-side updates with `updateGlassSelect()` - lifecycle helpers with `closeGlassSelect()`, `input$_open`, and `input$_ready` (TRUE once the widget has initialized) ## Basic usage ```{r basic} 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) ``` ## Initial selection ```{r initial} # No selection (default) glassSelect("f", fruits) # Pre-select one value glassSelect("f", fruits, selected = "banana") ``` ### Empty-string values are ordinary values 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 = "")`. ## Optional label ```{r label} glassSelect("f", fruits, label = "Choose a fruit") ``` ## Selection styles `glassSelect()` supports three selection marker styles: - `"checkbox"` (default) - `"check-only"` - `"filled"` ```{r styles} glassSelect("f1", fruits, check_style = "checkbox") glassSelect("f2", fruits, check_style = "check-only") glassSelect("f3", fruits, check_style = "filled") ``` ## Corner shape 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: ```{r shape} # 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: ```{r shape-comparison} fluidRow( column( 6, selectizeInput("native_region", "Native", choices = fruits) ), column( 6, glassSelect( "glass_region", fruits, label = "glasstabs", shape = "square", width = "100%" ) ) ) ``` ## Width, grouped choices, and disabled options 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. ```{r parity} # 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()`. ### Long selected labels 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. ```{r long-labels} 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. ## Dropdown lifecycle For dynamic layouts, modals, sidebars, or tab changes, you can explicitly close an open dropdown from the server: ```{r lifecycle} observeEvent(input$change_layout, { closeGlassSelect(session, "pick") }) ``` The widget also reports whether its dropdown is open: ```{r open-state} observe({ message("Dropdown open: ", isTRUE(input$pick_open)) }) ``` `closeAllGlassSelects(session)` closes every open glasstabs select dropdown in the current session. ## Searchable and clearable 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. ```{r search-clear} glassSelect( "f", fruits, searchable = TRUE, clearable = TRUE ) ``` 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: ```{r automatic-search} 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: ```{r update-automatic-search} updateGlassSelect( session, "category", searchable = "auto", search_threshold = 5 ) ``` Field and motion styling can be adjusted without changing the input value: ```{r presentation-options} 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: ```{r no-search} glassSelect( "f", fruits, searchable = FALSE ) ``` ## Server-side search for large choice sets 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. ```{r server-search} 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. ## Explicit "All" option You can prepend an explicit "All" option: ```{r include-all} 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. ## Server-side access Read the current value directly from `input$`: ```{r server-direct} server <- function(input, output, session) { observe({ message("Selected: ", if (is.null(input$pick)) "NULL" else input$pick) }) } ``` ## Reactive helper If you want a small convenience wrapper, use `glassSelectValue()`: ```{r server-helper} server <- function(input, output, session) { pick <- glassSelectValue(input, "pick") observe({ print(pick()) }) } ``` ## Updating choices and selection from the server `updateGlassSelect()` can update: - available choices - current selection - selection marker style ```{r style selection} observeEvent(input$filled, { updateGlassSelect( session, "pick", check_style = "filled" ) }) ``` It follows Shiny-style update semantics: - `choices = NULL` leaves choices unchanged - `selected = NULL` leaves selection unchanged - `selected = character(0)` clears the selection ```{r updates} ui <- 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()`. ## Theming ### Built-in presets ```{r presets} 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. ### Custom theme with `glass_select_theme()` ```{r custom-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" ) ) ``` ## Standalone usage `glassSelect()` can be used without the tab widget. You only need `useGlassTabs()` to load the package CSS and JavaScript assets: ```{r standalone} ui <- fluidPage( useGlassTabs(), glassSelect("region", c(North = "North", South = "South", East = "East")), verbatimTextOutput("out") ) server <- function(input, output, session) { output$out <- renderPrint(input$region) } if (interactive()) shinyApp(ui, server) ```