--- title: "Theming and light/dark" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Theming and light/dark} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` # Basics ```{r} 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()`. # Presets | `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. # Runtime updates from the server ```{r} 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. # Theme toggle helper ```{r} 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. # Behavior knobs ```{r} 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. # Plot and table surfaces 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. ```{r} ui <- fluidPage( theme = glass_theme(plot_surface = "opaque"), glass_plot_surface_input(), plotOutput("p"), DT::DTOutput("tbl") ) ``` # Public CSS tokens `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: ```{r} 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: ```{r} glass_intensity_slider("glass_intensity", value = 0.5) ``` 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. # CSS variables (migration from 0.1.x) **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: ```css :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. # Client helpers ```js 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" }); ``` # Teal ```{r} options(teal.bs_theme = glass_theme(preset = "auto")) # then teal::init(...) as usual ``` # Persistence `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. # Wallpaper scenes `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()]. # Print / export flatten 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`: ```{r} 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. # Plots that match the chrome ```{r} 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. # Reduced motion When the user prefers reduced motion (`prefers-reduced-motion: reduce`): - Navbar morph transforms are disabled (JS + CSS) - Pointer specular tracking is skipped - Content tint sampling is skipped - Decorative transitions on cards/inputs/nav are removed 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: ```{r eval=FALSE} glass_theme(tint = FALSE, specular = FALSE, nav_morph = FALSE, ambient_motion = FALSE) ``` This retains glass surfaces and blur; it is not a zero-paint-cost mode. # Multiple intensity controls 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. # Active-on-accent contrast 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.