library(shiny)
library(shinyglass)
ui <- glass_page(
title = "Liquid Glass",
persist = TRUE,
scene = "tahoe",
plotOutput("p")
)glass_page() is fluidPage() plus
[glass_theme()], the Light / Dark / Auto toggle, intensity slider, and
accent wells. Pair it with [observe_glass()] in the server. For a bare
page, still pass theme = glass_theme() to
fluidPage() / navbarPage() /
bslib::page_sidebar().
preset |
Behavior |
|---|---|
"light" |
Light glass pack (default) |
"dark" |
Dark glass pack |
"auto" |
Follows prefers-color-scheme; updates when the OS theme
changes |
Light and dark surface tokens ship as dual CSS custom-property packs
on [data-glass-preset]. Switching preset updates
document.documentElement.dataset.glassPreset — no Sass
recompile and no full page reload.
ui <- fluidPage(
theme = glass_theme(preset = "auto"),
glass_theme_toggle(selected = "auto"),
selectInput("accent", "Accent", c(
Blue = "#007AFF", Purple = "#AF52DE", Orange = "#FF9500"
))
)
server <- function(input, output, session) {
observe_glass_theme_toggle(input, session)
observeEvent(input$accent, {
update_glass_theme(session, primary = input$accent)
}, ignoreInit = TRUE)
}update_glass_theme() accepts:
| Argument | Effect |
|---|---|
preset |
"light" / "dark" /
"auto" |
tint |
Content-aware ambient color from plots/images |
primary |
Live accent via CSS variables (--bs-primary, …) |
intensity |
Ultra Clear (0) → Tinted (1) |
material |
"regular" / "clear" |
plot_surface |
"clear" / "opaque" |
scene |
Named wallpaper pack (glass_scenes()) |
flatten |
Print / capture mode (see [glass_flatten()]) |
tokens |
Named --glass-* overrides ([glass_css_tokens()]) |
primary updates Bootstrap accent CSS variables so
buttons, checked controls, and other accent surfaces follow without a
reload. A few Sass-baked one-offs may still need a full reload to pick
up a new color.
ui <- fluidPage(
theme = glass_theme(preset = "auto"),
glass_theme_toggle(), # Light / Dark / Auto buttons
# ...
)
server <- function(input, output, session) {
observe_glass_theme_toggle(input, session)
}Buttons call window.shinyglass.setPreset() immediately
and also fire Shiny inputs (glass_toggle_light, …) so the
server can stay in sync.
glass_theme(
preset = "auto",
primary = "#AF52DE",
blur = 36,
saturation = 200,
radius = "1.5rem",
material = "regular", # or "clear" over media-rich content
plot_surface = "clear", # or "opaque" for dense charts/tables
intensity = 0.45, # 0 Ultra Clear → 1 Tinted (iOS 27)
tint = TRUE, # sample plot/image colors into glass surfaces
specular = TRUE, # pointer specular highlight
nav_morph = TRUE # compact navbar while scrolling down
)material = "regular" is the adaptive Tahoe-style fill
(default). Use "clear" when chrome sits over rich media and
labels stay bold.
plot_surface = "clear" is the default: plot and table
hosts stay translucent so the wallpaper shows through. Use
"opaque" when dense charts or tables need a regular ~94%
panel (HIG: glass for nav/controls, stronger materials for content).
That is also the CSS class .glass-plot-surface-opaque on a
host or ancestor, and
update_glass_theme(session, plot_surface = "opaque") /
window.shinyglass.setPlotSurface("opaque") switch it
live.
Apple HIG keeps Liquid Glass on chrome (nav, sidebars, controls) and uses a denser material for content that must stay readable. shinyglass follows that split:
| Host | Clear (default) | Opaque |
|---|---|---|
ggplot (theme_glass()) |
Transparent paper | ~94% panel fill |
plotly (plotly_glass()) |
Transparent paper/plot | Same paper token |
gt (gt_theme_glass()) |
Transparent table chrome | Panel + header wash |
| DT | CSS host is translucent | --glass-plot-panel fill |
glass_plot_surface_input() is the drop-in control.
Optional dt_options_glass() keeps DataTables inside the
card (scrollX, wrapping headers) — the visible skin is
still package CSS, not a DT theme dialect.
glass_css_tokens() lists stable --glass-*
variables. Override them without forking SCSS:
th <- glass_theme(tokens = list(blur = "28px", radius = "1.25rem"))
# or later:
# th <- glass_add_tokens(th, list(`--glass-bg` = "rgba(255,255,255,0.3)"))glass_token_pack("light") /
glass_token_pack("dark") returns the default values for a
pack. Live updates:
update_glass_theme(session, tokens = list(blur = "40px"))
or
window.shinyglass.setTokens({ "--glass-blur": "40px" }).
Use [glass_intensity_slider()] for a live Ultra Clear → Tinted control. It mirrors iOS 27 Settings → Appearance → Liquid Glass. The web cannot read that OS slider, so the in-app control is intentional — it is the supported way to match the system look from a Shiny app:
All three JS behaviors default to TRUE (same as 0.1.x).
They respect prefers-reduced-motion: reduce where relevant.
Advanced escape hatch:
window.__shinyglassDisableTint = true still disables
tint.
Breaking in 0.2.0: light/dark surfaces are dual CSS
packs, not a Bootswatch darkly rebuild. Prefer CSS custom
properties over Sass $glass-* tokens when customizing:
:root {
/* shared knobs also set by glass_theme(blur=..., radius=...) */
--glass-blur: 36px;
--glass-radius: 1.25rem;
--bs-primary: #007AFF; /* also updated by update_glass_theme(primary=) */
--glass-primary: #007AFF;
}
:root[data-glass-preset="dark"] {
/* override a dark-pack value if needed */
--glass-bg: rgba(255, 255, 255, 0.1);
}Apps that overrode Sass-only $glass-* tokens should
target --glass-* or [data-glass-preset="dark"]
instead.
window.shinyglass.setPreset("dark"); // or "light" / "auto"
window.shinyglass.getPreset(); // resolved "light" | "dark"
window.shinyglass.getMode(); // requested mode incl. "auto"
window.shinyglass.setTint(false);
window.shinyglass.setPrimary("#AF52DE");
window.shinyglass.getPrimary();
window.shinyglass.setPlotSurface("opaque"); // or "clear"
window.shinyglass.getPlotSurface();
window.shinyglass.setScene("aurora");
window.shinyglass.enterFlatten();
window.shinyglass.setTokens({ "--glass-blur": "28px" });glass_theme(persist = TRUE) (and
glass_page(), which defaults to on) writes preset,
intensity, accent, material, plot surface, and scene to
localStorage for this app path so a refresh restores the
last look.
glass_theme(scene = "tahoe") (or "dusk" /
"mesh" / "aurora" / "harbor" /
"grove") swaps the page gradient and orbs. See
[glass_scenes()] for ids and labels.
wallpaper = "https://..." paints a frosted photo behind the
glass: the photo is blurred and washed
(--glass-wallpaper-wash) so body ink (#1d1d1f
light / #f5f5f7 dark) stays above a 4.5:1 floor on glass
surfaces and a 3:1 floor on large chrome. Do not place raw body text on
an unwashed photo. Change scene live with
update_glass_theme(session, scene = "aurora") or
[glass_scene_input()].
Backdrop blur and translucent fills break chromote screenshots,
print-to-PDF, and static HTML. Enter flatten to swap glass fills to the
opaque menu pack and drop backdrop-filter:
glass_flatten(session, TRUE) # or update_glass_theme(session, flatten = TRUE)
# window.shinyglass.enterFlatten()
# ?glass_flatten=1@media print applies the same rules automatically. Call
glass_flatten(session, FALSE) or
window.shinyglass.exitFlatten() to restore live glass.
output$p <- renderPlot({
ggplot2::ggplot(mtcars, ggplot2::aes(wt, mpg)) +
ggplot2::geom_point() +
theme_glass(input = input)
}, bg = "transparent")[theme_glass()] is a ggplot2 theme with transparent panels and
light/dark ink. [plotly_glass()] and [gt_theme_glass()] do the same for
those packages. [glass_plot_colors()] returns ink,
grid, fill, and paper. Pass
surface = "opaque" (or
glass_theme(plot_surface = "opaque")) when the panel itself
should be a regular fill instead of show-through glass.
When the user prefers reduced motion
(prefers-reduced-motion: reduce):
The scroll-edge toolbar densify (body.glass-scroll-edge)
stays on: it is a contrast treatment for content under floating bars,
not a motion effect. Reduced transparency still forces the Tinted
intensity endpoint.
This is automatic and responds to OS preference changes while the app is open. Reduced-transparency changes also update the surfaces without changing the requested slider value.
To disable only the idle decorative animation, use
glass_theme(ambient_motion = FALSE). To disable all
optional effects, use:
This retains glass surfaces and blur; it is not a zero-paint-cost mode.
Intensity is page-wide. The first explicit slider value
in each newly inserted group sets that intensity; peer sliders
synchronize visually. A restricted-range slider displays the nearest
endpoint when the global value is outside its range. Bounds must satisfy
0 <= min < max <= 1, step must be
finite and positive, and an explicit value must be within
the slider’s range.
Bootstrap’s color-contrast() often picks
black ink for system blue #007AFF.
glass_theme() forces light ink on primary fills so checked
checkboxes, radios, switches, and active pagination retain light ink.
White on #007AFF is approximately 4.02:1: it meets the 3:1
large-text threshold, but not the 4.5:1 ordinary-text threshold. Choose
and test an appropriate darker accent when ordinary white labels must
meet that threshold. The QA guide’s --text-aa audit checks
size-aware text thresholds; translucent backgrounds, gradients, and
focus visibility still need visual review.