glasstabs provides animated Shiny widgets for tab navigation and selection controls:
All widgets work in plain fluidPage() and in bs4Dash.
They can be used together or independently.
Start here, by task. Only need tabs? Read the tabs article. Only need a dropdown? Read the single-select or multi-select guide. Building reusable filters? Read the modules and linked-filters recipes. Something broken? Read troubleshooting first — most entries are configuration choices, not bugs. The example gallery lists a runnable demo for each question above. —
# From CRAN
install.packages("glasstabs")
# From GitHub
pak::pak("PrigasG/glasstabs")
# From source
devtools::install_local("path/to/glasstabs")useGlassTabs() onceEvery glasstabs app needs exactly one call to
useGlassTabs() somewhere in the UI. It injects the CSS and
JavaScript as a proper htmltools dependency.
library(shiny)
library(glasstabs)
ui <- fluidPage(
useGlassTabs(), # <-- this is all you need
# ... rest of your UI
)Each glassTabPanel() takes a unique value,
a display label, and any UI content:
glassTabsUI()Pass your panels to glassTabsUI() along with a namespace
id:
ui <- fluidPage(
useGlassTabs(),
glassTabsUI("nav",
glassTabPanel("overview", "Overview", selected = TRUE,
shiny::h3("Welcome"),
shiny::p("Start here.")
),
glassTabPanel("analysis", "Analysis",
shiny::h3("Analysis"),
shiny::p("Your charts go here.")
),
glassTabPanel("settings", "Settings",
shiny::h3("Settings"),
shiny::p("Configuration options.")
)
)
)server <- function(input, output, session) {
output$selected <- renderPrint(input$category)
}
if (interactive()) shinyApp(ui, server)input$category is always a plain character vector of the
checked values — use it like any other Shiny input to filter data, drive
outputs, or trigger reactives.
For the complete selection model, summaries, linked-filter pattern, server search, transactional updates, and migration notes, continue with the multi-select guide.
For long filter names, give the field more room with
width = "240px" or width = "100%". With
overflow_tooltip = "auto", hovering a truncated option
inside the open dropdown reveals that option alone. The trigger retains
the complete accessible selection label without showing one combined
tooltip for multiple values. These controls affect appearance only, not
the value returned to Shiny.
If you want a small convenience wrapper:
For the full single-value API and examples, see the single-select guide.
server <- function(input, output, session) {
output$selected <- renderPrint(input$region)
}
if (interactive()) shinyApp(ui, server)input$region is a single character value, or
NULL when nothing is selected.
The single- and multi-select widgets share tongue,
label_bold, label_size,
search_focus_ring, and overflow_tooltip. This
makes it possible to keep the glass animation, use the quieter detached
transition, or match native Shiny labels without changing server code.
Multi-selects add fill_colors = "single" (default) or
"multi" for filled markers.
They also share automatic search. With
searchable = "auto", the search field is hidden until the
number of choices reaches search_threshold; this keeps a
five-category field simple while still supporting a list that grows
later.
A common pattern is using one or more filters to drive content inside
tab panes. Pass a widget to extra_ui and place
glassFilterTags() inside panes to show active multi-select
filters as removable tag pills.
choices <- c(North = "north", South = "south", East = "east", West = "west")
ui <- fluidPage(
useGlassTabs(),
glassTabsUI("main",
extra_ui = glassMultiSelect(
inputId = "region",
choices = choices,
show_style_switcher = FALSE
),
glassTabPanel("summary", "Summary", selected = TRUE,
shiny::h3("Summary"),
glassFilterTags("region"), # tag pills appear here
shiny::uiOutput("summary_text")
),
glassTabPanel("detail", "Detail",
shiny::h3("Detail"),
glassFilterTags("region"), # same filter, second pane
shiny::tableOutput("detail_table")
)
)
)
server <- function(input, output, session) {
# Fall back when an input is NULL or empty (no selection yet).
fallback <- function(x, default) if (is.null(x) || length(x) == 0) default else x
selected_regions <- reactive({
fallback(input$region, unique(unname(choices)))
})
output$summary_text <- renderUI({
shiny::p("Showing data for: ",
shiny::strong(paste(selected_regions(), collapse = ", ")))
})
output$detail_table <- renderTable({
data.frame(Region = selected_regions())
})
}
if (interactive()) shinyApp(ui, server)Both widgets default to "dark". Switch to
"light" or supply a custom theme object — in each case you
only override what you need:
# Built-in light preset
glassTabsUI("nav", theme = "light", ...)
glassMultiSelect("f", theme = "light", ...)
# Custom — one field each
glassTabsUI("nav",
theme = glass_tab_theme(halo_bg = "rgba(251,191,36,0.15)"),
...
)
glassMultiSelect("f", choices,
theme = glass_select_theme(accent_color = "#f59e0b")
)Add wrap = FALSE so the glass halo positions itself
relative to the card body rather than a full-page container. Pair with
theme = "light":
library(bs4Dash)
library(glasstabs)
choices <- c(Alpha = "alpha", Beta = "beta", Gamma = "gamma")
ui <- bs4DashPage(
header = bs4DashNavbar(title = "My App"),
sidebar = bs4DashSidebar(disable = TRUE),
body = bs4DashBody(
useGlassTabs(),
bs4Card(
title = "Analysis", width = 12,
glassTabsUI("dash",
wrap = FALSE,
theme = "light",
extra_ui = glassMultiSelect(
"f",
choices,
theme = "light",
show_style_switcher = FALSE
),
glassTabPanel("a", "Overview", selected = TRUE,
shiny::p("Overview content.")
),
glassTabPanel("b", "Detail",
shiny::p("Detail content.")
)
)
)
)
)
server <- function(input, output, session) {}
if (interactive()) shinyApp(ui, server)Most apps only need one setup call, one navigation widget, and the controls that belong near it. This compact pattern is ready to adapt:
ui <- fluidPage(
useGlassTabs(),
glassTabsUI(
"main",
glassTabPanel("summary", "Summary", selected = TRUE, summary_ui),
glassTabPanel("details", "Details", details_ui),
extra_ui = glassSelect(
"region",
c(North = "north", South = "south"),
clearable = TRUE,
theme = "auto"
),
theme = "auto"
)
)
server <- function(input, output, session) {
active_tab <- glassTabsServer("main")
observe({
region <- if (is.null(input$region)) "all" else input$region
message("Tab: ", active_tab(), "; region: ", region)
})
}Note that mapping an empty single-select to "all" here
is an explicit app choice, not the widget’s behavior: a cleared
single-select returns NULL, and this app deliberately reads
NULL as “all”. The multi-select guide’s “Understanding
selection behavior” section works through the same decision for
multi-selects, where the trap is Clear all restoring every record.
From here, add only the options the app actually needs. Every widget
keeps the usual Shiny model: place it in the UI, read its value from
input, and use an update helper when the server needs to
change it.