Call useGlassTabs() once somewhere in your UI before
using any widget.
glassPage() is the experimental exception. It is a thin
wrapper around bslib::page_fillable() and loads the
dependency itself.
The key rule: use ns() in the UI, use the
bare id in the server.
# UI function
my_ui <- function(id) {
ns <- NS(id)
tagList(
useGlassTabs(),
glassTabsUI(
ns("tabs"), # ns() wraps the id in the UI
glassTabPanel("summary", "Summary", selected = TRUE, h3("Summary")),
glassTabPanel("detail", "Detail", h3("Detail"))
)
)
}
# Server function
my_server <- function(id) {
moduleServer(id, function(input, output, session) {
active <- glassTabsServer("tabs") # bare id — no ns() here
observeEvent(active(), {
message("Active tab: ", active())
})
})
}
# App
ui <- fluidPage(my_ui("explorer"))
server <- function(input, output, session) my_server("explorer")
if (interactive()) shinyApp(ui, server)# Build panels from data
tab_defs <- list(
list(value = "revenue", label = "Revenue"),
list(value = "orders", label = "Orders"),
list(value = "returns", label = "Returns")
)
panels <- lapply(tab_defs, function(t) {
glassTabPanel(t$value, t$label, h3(t$label))
})
ui <- fluidPage(
useGlassTabs(),
do.call(glassTabsUI, c(list("metrics", selected = "orders"), panels))
)
server <- function(input, output, session) {
active <- glassTabsServer("metrics")
}Note: tab
valuestrings must be unique —glassTabsUI()will error on duplicates. Values are plain strings; they do not need to match labels.
# Switch active tab
updateGlassTabsUI(session, "main", "details")
# Hide or show a tab
hideGlassTab(session, "main", "admin")
showGlassTab(session, "main", "admin")
# Append or remove a tab at runtime
appendGlassTab(
session, "main",
glassTabPanel("compare", "Compare", h3("Compare")),
select = TRUE
)
removeGlassTab(session, "main", "compare")glassTabsUI(
"main",
glassTabPanel("a", "A", selected = TRUE, p("A")),
glassTabPanel("b", "B", p("B")),
selected = "a",
wrap = TRUE,
compact = FALSE, # set TRUE inside dashboard cards for tighter layout
content_min_height = NULL, # default: 120px (60px when compact = TRUE); "0" for no minimum
extra_ui = tags$div("Right side UI"),
theme = "light"
)Use compact = TRUE when embedding glasstabs inside a
tight layout such as a bs4Dash::bs4Card(). It reduces
margins, padding, and font size so the widget does not overflow the
card.
The active tab and its content are visually linked automatically: the
active tab carries a subtle inner light on its content-facing edge
(bottom for horizontal tabs, the rail side for vertical, RTL-aware) in
the halo color, and the content box’s top edge picks up the same color
to bridge the gap. The underline indicator keeps its own
connection cue instead. No parameters needed - to restyle the link,
theme halo_border via glass_tab_theme().
Use style = "attached" for a single unified card where
the tab bar docks onto the content box, and
transition = "slide" for directional pane movement that
follows tab order.
glassMultiSelect(
"metric",
choices,
selected = unname(choices),
label = "Metrics",
placeholder = "Choose metrics",
all_label = "All metrics",
check_style = "checkbox",
show_style_switcher = TRUE,
show_select_all = TRUE,
show_clear_all = TRUE,
searchable = "auto",
search_threshold = 12,
selection_display = "summary",
selection_max_items = 2,
dropdown_max_height = "18rem",
theme = "dark",
no_matches_text = "No matches" # last argument, so positional calls keep working
)glassTabsUI() works inside
bs4Dash::tabItem(). Place useGlassTabs() in
the dashboardBody() (or anywhere before the first widget —
once is enough).
library(shiny)
library(bs4Dash)
ui <- bs4DashPage(
header = bs4DashNavbar(),
sidebar = bs4DashSidebar(
bs4SidebarMenu(
bs4SidebarMenuItem("Explorer", tabName = "explorer", icon = icon("table"))
)
),
body = bs4DashBody(
useGlassTabs(), # place once, anywhere in the body
bs4TabItems(
bs4TabItem(
tabName = "explorer",
bs4Card(
title = "Data explorer",
width = 12,
glassTabsUI(
"s3_tabs",
glassTabPanel("buckets", "Buckets", selected = TRUE,
p("List of S3 buckets here.")
),
glassTabPanel("objects", "Objects",
p("Object browser here.")
),
glassTabPanel("preview", "Preview",
p("File preview here.")
)
)
)
)
)
)
)
server <- function(input, output, session) {
active <- glassTabsServer("s3_tabs")
}
if (interactive()) shinyApp(ui, server)
useGlassTabs()only needs to appear once. Inside a module that renders into a bs4Dash layout, call it in your module’s UI function.
| Widget | Server value |
|---|---|
glassTabsUI("main", ...) |
input[["main-active_tab"]] or
glassTabsServer("main")() |
glassMultiSelect("metric", ...) |
input$metric or
glassMultiSelectValue(input, "metric")$selected() |
| resolved multi-select value | glassMultiSelectValue(..., empty_behavior = "all")$resolved() |
| multi-select style | input$metric_style or
glassMultiSelectValue(input, "metric")$style() |
| select open state | input$metric_open / input$region_open |
glassSelect("region", ...) |
input$region or
glassSelectValue(input, "region")() |
Use glassTabCondition() to avoid constructing the input
key manually:
# Instead of: condition = "input['main-active_tab'] === 'details'"
conditionalPanel(
condition = glassTabCondition("main", "details"),
p("Only visible on the Details tab.")
)
# Inside a module — pass the same id as glassTabsUI():
# glassTabsUI(ns("tabs"), ...)
conditionalPanel(
condition = glassTabCondition(NS("mymod")("tabs"), "details"),
p("Only visible on the Details tab.")
)useGlassTabs() must be called once in the UI.ns("tabs") in the UI but pass the bare
"tabs" to glassTabsServer() — not
ns("tabs"). Passing a namespaced id (containing
-) will produce a warning with a suggested fix.glassTabPanel() values must be unique within a
glassTabsUI() call — duplicates cause an error naming the
offending value.selected in glassTabsUI() refers to the
tab value, not the label.glassSelectValue() returns a reactive function, not a
list.glassMultiSelectValue() returns raw
selected(), resolved(),
is_empty(), and style() reactives.glassMultiSelect(), selected should
use choice values, not labels.compact = TRUE inside bs4Dash cards or any tight
layout to reduce widget spacing.load_all() before retesting.That is the whole working model: load the dependency once, compose widgets in the UI, and read their values through ordinary Shiny inputs.