13 KiB
Glass Shell design spec
Goal: re-skin every surface and sub-menu of the Steam Frame's VR interface (Steam gamepadui as SteamVR overlays) into Apple's glass design language, with zero functional change.
- visionOS window glass for the window.
- Liquid Glass for the floating controls.
Source material is the project's design bible (../bible/, published at https://claude.ai/artifact/T4AqVM5FQrQzn3kW4UTYe9) and its rulebook ../.claude/skills/run-liquid-glass-frame/references/design-language.md. This spec translates them to CSS inside Steam's CEF.
1. What the platform allows
- Overlays have per-pixel alpha. Every surface is a quad composited over the room (passthrough, mono or colour) or over the running game.
- Transparent pixels show the world.
- Translucent pixels tint it.
backdrop-filteronly sees the page's own content, never the room. So:- Window glass over the room = translucent smoky tint + specular rim + sheen. It cannot blur the room, so the tint must carry legibility on its own (
--lgs-window-alpha≈ 0.72 by default; the user dial moves it from 0.60 to 0.84). - Liquid Glass over in-page content =
backdrop-filterblur + saturation (+ lensing later). Examples: header capsules over scrolling library art, side menus and sheets over the window, context menus over a page.
- Window glass over the room = translucent smoky tint + specular rim + sheen. It cannot blur the room, so the tint must carry legibility on its own (
- Each popup is its own quad.
- The bar segments, bar popups, frame menus, tooltip, volume HUD, footer, toasts and keyboard float alone over the room. They use panel glass: tint + rim + sheen, no backdrop blur (there is nothing in-page behind them).
- Their surroundings are transparent, so rounded corners are real.
- The Adreno GPU is shared with the compositor.
- Steam's CEF runs GPU compositing and GPU raster (ANGLE on zink/Turnip). It repaints on change, so static decoration is nearly free.
- Stock Steam already has about 100 elements with
backdrop-filteron library home and scrolls at about 85 fps, so blur is affordable. It is still not free: blurred backdrops re-render while what's behind them moves. - Budget, measured:
python glass.py perf main --route Rscrolls the page with the theme off, then on. Themed must keep the same fps (within 5%) and add no long frames (>34 ms). - No animated filters, no infinite animations.
2. Materials (all defined in theme/00-tokens.nowrap.css)
| Material | Use for | Recipe (copy these declarations) |
|---|---|---|
| Window glass | The dashboard window's root background, i.e. whatever paints the opaque page background today | background: var(--lgs-window-sheen), var(--lgs-window-bg); box-shadow: var(--lgs-window-rim); border-radius: var(--lgs-r-window); |
| Panel glass | Standalone overlays: bar segments, bar popups, frame menus, toasts, tooltip, HUDs, footer, keyboard | background: var(--lgs-panel-sheen), var(--lgs-panel-bg); box-shadow: var(--lgs-panel-rim); border-radius: var(--lgs-r-panel) (or --lgs-r-capsule / --lgs-r-menu by shape) |
| Liquid Glass (regular) | Floating controls over in-page content: header and search capsule, segmented tab bars, floating action capsules | background: var(--lgs-glass-bg); backdrop-filter: var(--lgs-glass-blur); box-shadow: var(--lgs-glass-rim), var(--lgs-glass-shadow); border-radius: var(--lgs-r-capsule); |
| Thick glass | Sheets, modals, side menus, context and dropdown menus inside the main window | background: var(--lgs-panel-sheen), var(--lgs-thick-bg); backdrop-filter: var(--lgs-thick-blur); box-shadow: var(--lgs-glass-rim), var(--lgs-glass-shadow); border-radius: var(--lgs-r-sheet) (menus --lgs-r-menu). Modal scrim: --lgs-scrim |
| Fills (content on glass) | Grouped settings sections, list rows at rest, input wells, chips, non-selected segments | --lgs-fill-1/2/3, --lgs-fill-sunken for wells; separators --lgs-separator. Never glass on glass: anything inside a glass container uses fills, not another blur |
Rim and sheen are the Liquid Glass signature: a crisp bright top edge, faint side edges and a soft top-down sheen. Light comes from above, the same direction everywhere. Don't add outer drop shadows on standalone overlays, which have no room for them; inside the main window, floating glass may use --lgs-glass-shadow.
Content is not glass. Game art, capsules, screenshots, hero images, avatars and store pages stay opaque content.
- Round their corners to the concentric radius (
--lgs-r-cardfor capsules,--lgs-r-smallfor small art). - Never tint or blur them.
3. Interaction states (must stay unmistakable)
| State | Treatment |
|---|---|
| Rest | Transparent, or --lgs-fill-1 for rows and grouped items; labels --lgs-text-1, secondary --lgs-text-2 |
| Hover (laser) | --lgs-hover-fill, transition var(--lgs-t-fast) var(--lgs-ease) |
Focus (.gpfocus, controller and laser focus) |
background: var(--lgs-focus-fill); box-shadow: var(--lgs-focus-ring);. It "illuminates from within". Must be visible on every focusable control in every surface. For content like game capsules, use --lgs-focus-ring-outer around the art and keep Steam's scale-up if it has one |
| Selected (active tab, current nav item, chosen segment) | White fill --lgs-selected-fill + dark label --lgs-text-on-selected. Reserved: nothing else is white-filled |
| Pressed | Brighter fill; optional scale: var(--lgs-pressed-scale), using the independent property, never transform |
| Disabled | opacity: var(--lgs-disabled-opacity) on the control, or --lgs-text-disabled for labels |
| Primary action | Only the one primary action per screen is tinted: Play is --lgs-tint-play, a dialog's confirm is --lgs-tint-primary, destructive is --lgs-tint-danger. Tint goes into the whole capsule fill with a white label, never into thin text |
4. Components
- Buttons (
DialogButtonfamily, bar small buttons): capsule (--lgs-r-capsule) for text buttons, circle for icon buttons.- Rest:
--lgs-fill-2with an inner top riminset 0 1px 0 rgba(255,255,255,.18). - Focus: per §3.
- Keep sizes; only round and recolour them.
- Rest:
- Toggles:
- Track: capsule, off
--lgs-fill-3, on--lgs-toggle-on(whole fill). - Knob: white with a soft shadow
0 1px 3px rgba(0,0,0,.3). - Focused toggle row gets the focus treatment.
- Track: capsule, off
- Sliders:
- Track:
--lgs-fill-3capsule. - Fill:
rgba(255,255,255,.92). - Knob: white, with a subtle ring.
- Notches dim.
- Track:
- Dropdown buttons look like capsule buttons. Opened dropdown menus are thick-glass menus.
- Text inputs / search: a sunken capsule well (
--lgs-fill-sunkenwithinset 0 1px 2px rgba(0,0,0,.25)), white text,--lgs-text-3placeholder. Focus: ring. - Field rows (settings and dialogs): transparent rows inside a grouped section card (
--lgs-fill-1,--lgs-r-card).- Separators
--lgs-separator, inset from the left. - The focused row gets focus fill + ring with
--lgs-r-row.
- Separators
- Tabs / segmented controls: a Liquid Glass capsule holding the segments; the selected segment is the white pill.
- Steam's home tabs (WHAT'S NEW / FRIENDS / RECOMMENDED) and library tabs map to this.
- Keep their text case and size.
- Menus (context menus, dropdowns, bar popups, frame menus): thick or panel glass,
--lgs-r-menu, 6–8 px inner padding.- Rows
--lgs-r-row. - Headers in
--lgs-text-2, small caps-free title case. - Separators as hairlines.
- Materialize with
animation: lgs-materialize var(--lgs-t) var(--lgs-spring) both;, only on the menu container and only if Steam doesn't already animate it withopacity/transform.
- Rows
- Modals / sheets: thick glass,
--lgs-r-sheet, scrim--lgs-scrim. - Toasts and notifications: panel glass rounded rect,
--lgs-r-panel; icon left, title--lgs-text-1, body--lgs-text-2. - Scrollbars: thin capsule thumbs,
rgba(255,255,255,.28), if Steam shows any. - The dashboard bar (ornament):
- Each
BarSurfacesegment becomes a panel-glass capsule or rounded slab. - Icons are white.
- The active tab gets a white-ish selected fill or an underglow dot.
- The + and small buttons are circular glass hit areas with the focus treatment.
- The clock uses
--lgs-text-1. - The avatar has a rounded square.
- Each
- The keyboard: a panel-glass slab.
- Keys are fills (
--lgs-fill-2,--lgs-r-small) with white labels; special keys--lgs-fill-3. - The focused or touched key is bright white fill with a dark label (visionOS keyboard style).
- Enter is tinted
--lgs-tint-primary.
- Keys are fills (
5. Type and colour
- Keep Steam's font (Motiva Sans) and its sizes. Don't change font sizes or weights in a way that can reflow and clip text; a +100 weight on small labels is fine where space allows.
- Labels are white at vibrancy levels:
--lgs-text-1/2/3.- Map Steam's greys: near-white →
--lgs-text-1, mid grey →--lgs-text-2, dim grey →--lgs-text-3. - Add
text-shadow: var(--lgs-text-shadow)on text that sits directly on window or panel glass.
- Map Steam's greys: near-white →
- Colour only in whole fills: the green Play capsule, the green toggle-on track, blue primary confirm, red destructive, badges.
- Keep Steam's semantic colours where they carry meaning (online green, in-game blue), but as fills or dots.
6. Shape
Concentric: inner radius = outer radius − padding.
| Element | Radius |
|---|---|
| Window | 32 |
| Sheets | 28 |
| Panels | 24 |
| Menus | 20 |
| Cards and capsule art | 16 |
| Rows | 12 |
| Small | 8 |
| Controls | capsule |
7. Motion
- Only on state changes, never at rest.
- Hover and focus fills transition over 140 ms.
- Popups and menus materialize in 220 ms with a spring.
- No shimmer, no pulsing, no oscillation.
prefers-reduced-motionsets the durations to 1 ms.
8. Functionality guardrails (from docs/LAB.md, repeated because they matter most)
Never:
- hide, collapse, zero-opacity or disable pointer events on Steam elements;
- change layout geometry (position, size, overflow, flex/grid) of containers;
- override transforms Steam uses for positioning, carousels or virtualization;
- cover controls with pseudo-elements that take pointer events.
Run python glass.py audit SURF --route R on every screen you style. It must report 0 GONE / HIDDEN / SHRUNK / UNCLICKABLE and 0 CONTRAST.
Every focusable control must show the focus state in screenshots taken with that control focused (.gpfocus).
9. SteamVR's own pages
SteamVR draws a few pieces of the Frame UI itself; see LAB.md, "SteamVR's own pages". They follow the same language:
| Piece | Treatment |
|---|---|
| Window grab bar under the dashboard window | A Liquid Glass pill: a translucent white fill with a rim, brighter on hover or focus |
| Resize corners | Thin white strokes at --lgs-text-2 |
| Frame controls (keyboard, pop-out, recenter, more) | Panel-glass capsule segments, white icons, circular hit areas, obvious focus and hover. They sit as their own quads over the room |
| Controller status | A panel-glass card. Battery levels as whole-fill colour (green, orange, red) |
| Now Playing | Window glass like the dashboard. The game art is content. Resume Game is the one tinted primary (--lgs-tint-play); the other actions are fill capsules; Exit Game gets a destructive tint only on focus |
| Controller bindings / SteamVR settings pages | Window glass with the same primitives look as Steam's settings: fills, capsules, white selected, focus ring |
| Message overlays and toasts | Panel glass, like Steam's toasts |
systemui is an atlas page: everything outside the panels is transparent, and each panel becomes its own quad. Don't paint the page or vsg-app backgrounds.
10. File ownership
| File | Owner |
|---|---|
theme/00-tokens.nowrap.css, theme/defs.svg |
Foundation (changes go through it) |
theme/10-primitives.css |
Foundation: generic Steam components used everywhere (DialogButton, gamepaddialog fields, toggles, sliders, dropdowns, text inputs, context menus, modals, generic focus) |
theme/20-shell.css |
Window chrome: root window glass, header and search, side menus, page transitions, in-window footer |
theme/30-bar.css |
Dashboard bar, bar popups, frame menus |
theme/35-hud.css |
Tooltip, volume HUD, floating footer, notification toasts, keyboard |
theme/40-library.css |
Library home, library tabs, collections, capsules, search results |
theme/50-appdetails.css |
Game pages, play bar, game menus, properties, running-app overview |
theme/60-settings.css |
Settings pages (layout-level styling; controls come from primitives) |
theme/70-social.css |
Friends and chat, profile, notifications, media, downloads, store chrome, power menu, other routes |
theme/vr/*.css |
SteamVR's own pages: systemui (frame controls, grab bar, Now Playing, settings panels), controller bindings, overlays |
Area files may refine primitives inside their own containers. Generic primitive changes belong in 10-primitives.css.