Skip to contents

This guide converts an existing app without changing its downstream code. The filter logic stays identical; only the input construction and the update calls change. The one trap that breaks migrations is the empty-selection default — it is covered first, because it decides the rest.

The trap: empty does not mean the same thing

Native Shiny and glasstabs disagree about the starting state:

Control selected = NULL means
selectInput(multiple = TRUE) Nothing selected
glassMultiSelect() Every enabled choice selected

A migration that copies selected = NULL across changes the app’s initial filter from “nothing” to “everything.” Decide which behavior the app had, and write it explicitly — do not let the default decide for you. The behavior-preserving migration below uses selected = character(0) to keep the native empty start.

Before and after: a multi-select filter

Before, with native Shiny:

library(shiny)

regions <- c(North = "north", South = "south", East = "east")
data <- data.frame(
  region = c("north", "south", "east", "north"),
  cases = c(12, 8, 5, 3)
)

ui <- fluidPage(
  selectInput(
    "region",
    "Region",
    choices = regions,
    multiple = TRUE,
    selected = NULL
  ),
  tableOutput("rows")
)

server <- function(input, output, session) {
  output$rows <- renderTable({
    data[data$region %in% input$region, , drop = FALSE]
  })
}

shinyApp(ui, server)

After, with glasstabs. The downstream filter is untouched:

library(shiny)
library(glasstabs)

regions <- c(North = "north", South = "south", East = "east")
data <- data.frame(
  region = c("north", "south", "east", "north"),
  cases = c(12, 8, 5, 3)
)

ui <- fluidPage(
  useGlassTabs(),
  glassMultiSelect(
    "region",
    choices = regions,
    label = "Region",
    selected = character(0),
    theme = "auto"
  ),
  tableOutput("rows")
)

server <- function(input, output, session) {
  output$rows <- renderTable({
    data[data$region %in% input$region, , drop = FALSE]
  })
}

shinyApp(ui, server)

The example above is complete: it defines its dataset, both app halves, and the app object, so it runs end to end.

Two changes beyond the widget swap: useGlassTabs() once at the UI root (the one rule), and selected = character(0) preserving the native empty start. If the app actually wants the glasstabs default (all enabled selected), drop the selected argument and keep the filter — but then say so in the filter’s comments, because the next reader will otherwise assume the native default.

For a single-select the swap is selectInput() (without multiple = TRUE) to glassSelect(), with the same useGlassTabs() addition. The cleared state of a single-select returns NULL in both worlds, so no default trap applies there.

Update function mapping

Native glasstabs Notes
updateSelectInput(session, id, choices, selected) updateGlassMultiSelect(session, id, choices, selected, ...) or updateGlassSelect(session, id, choices, selected, ...) Choose by widget, not by the old call
updateSelectizeInput(session, id, choices, selected) Same as above Server-side selectize becomes server = TRUE + the *Server() helper, not an update call
label argument on the native updaters No equivalent The glasstabs updaters take no label; a label change means recreating the widget with renderUI()
selected = NULL (keep) selected = NULL (keep) Same meaning on both sides for updates
selected = character(0) (clear) selected = character(0) (clear) Same meaning on both sides for updates

The update semantics that have no native counterpart are the transaction options: preserve_selection, drop_invalid, and notify on the multi-select updater (see the linked-filters article). A migration that passes only choices and selected behaves like the native updater; the extra options are opt-in.

What native selectize can do that glasstabs cannot

Be candid about the gaps before migrating a selectize-heavy app:

  • User-created choices (options = list(create = TRUE)): no equivalent. glasstabs choices come from the app, not the user.
  • Selectize plugins (options = list(plugins = ...)): no equivalent — no drag-and-drop sorting, no remove-button plugin (tag pills have their own remove affordance, but it is not the plugin).
  • Server-side selectize as a drop-in: the mechanism differs. Native selectize queries the server per keystroke through its own protocol; glasstabs server mode uses server = TRUE plus a *Server() helper with bounded results. The user-visible behavior is close, but the code is not interchangeable.

When a native control is still the better choice

  • The app needs user-created choices or a selectize plugin above.
  • The app must render a plain <select> with no JavaScript enhancement (for example, a no-JS fallback page).
  • The list is tiny, static, and already working: a migration buys styling and consistency, not behavior, and that trade is not always worth the review.

For everything else — searchable dropdowns, multi-select filters, themed dashboards, server-backed large lists — the migration above is the whole job: swap the constructor, add useGlassTabs(), pin the initial selection explicitly, and remap the update calls.