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) to Tinted (1). Default
0.5. |
material |
"regular". "clear" is deprecated (maps to
intensity 0.12) |
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", # "clear" is deprecated; use a low intensity
plot_surface = "clear", # or "opaque" for dense charts/tables
intensity = 0.5, # 0 Ultra Clear to 1 Tinted (iOS 27 midpoint)
tint = TRUE, # sample plot/image colors into glass surfaces
specular = TRUE, # static top and bottom highlights (no pointer tilt)
nav_morph = TRUE, # compact navbar while scrolling down
refraction = TRUE,
morph = TRUE,
press = TRUE,
scroll_edge = TRUE,
minimize_tabs = TRUE,
icon_style = "clear", # or "color" / "all"
floating_sidebar = FALSE
)material = "regular" is the adaptive fill (default).
material = "clear" is deprecated. Use a low
intensity (Ultra Clear is 0) when chrome sits
over rich media and labels stay bold. Omitting intensity
with material = "clear" still maps to 0.12 and
warns. What changed in 0.5.0, and how to sit closer to the previous
look, is vignette("migrating").
Reduce Transparency (the OS setting) paints opaque fills and turns
backdrop blur off, including before JavaScript runs. Increase Contrast
and forced colors override the inline glass variables. The in-app slider
does not have to move for those overrides to apply. Reduce Transparency,
Increase Contrast, and forced colors turn refraction off. Reduced motion
turns press springs and tab shrinking off. morph still
shows the active pill, without the spring, when motion is reduced.
refraction = TRUE is the Chromium squircle lens (index
of refraction about 1.5). Safari and Firefox ignore the displacement map
and keep blur plus tint. icon_style = "clear" is the
default for bar and button icons. Light mode uses near-black ink, not
pure white, so the glyph stays above the contrast floor.
floating_sidebar = TRUE is the opt-in iPadOS 26 detached
sidebar.
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, including the refraction cues --glass-refract-a,
--glass-refract-b, --glass-refract-brightness,
and --glass-refract-contrast. Those scale with intensity.
They are color and backdrop boosts, not a lens displacement. 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.