Liquid Glass · 透镜 Lens
Glass carries information. Liquid expresses continuous change. Light expresses state. Every interactive surface is the same lens: a button is an action lens, a search field an input lens, a knob a moving lens, a panel a container lens. The system is native web, zero runtime dependencies, WebGL2 optics with a CSS glass fallback.
Version bundled here: 1.1.0-showcase.1. Source: ruiqichenbiec/design-systems, MIT.
Read order
| When | Read |
|---|---|
| Before any visual decision | assets/design-system/DESIGN.md |
| Choosing or adding a component | assets/design-system/README.md and COMPONENTS.md |
| Adding a new kind of control | assets/design-system/SCOPE.md (admission rules) |
| Wiring the renderer or gestures | assets/design-system/index.d.ts |
| Entry map of the original repo | references/lens-start-here.md |
Contract
All visible content decomposes into seven elements. Do not invent an eighth.
| Element | Holds | Boundary |
|---|---|---|
| Background | scene colour, texture, image, static content beneath the glass | what the glass refracts; carries no interaction |
| Symbol / Text | icons, labels, numbers, ticks, checkmarks, caret | readable meaning; clickable marks must sit on a lens |
| Lens | circles, pills, knobs, fields, panels, transparent selection layers | the only interaction carrier; shape/size/colour may vary, optics must be shared |
| Well | option slots, tracks, progress channels, liquid chambers | concave path or capacity; may receive a lens, never a second interaction style |
| Emission | local light, fibre, flowing light, halo, drop hints | must state source, range, trigger; no full-screen glow as state |
| Liquid | tinted fill, surface, inflow/outflow, chart volume | volume must match the number it represents |
| Reflection variant | narrow edge reflection, environment reflection, tinted reflection, dispersion | attached to a lens or well, never a separate permanent white rim |
Rules that follow:
- Semantic HTML stays responsible for input, focus and state. Never replace text, focus or labels with a canvas.
- Every clickable, editable, selectable, adjustable, draggable area is carried by a lens component and its shared gesture physics. Reusing only the look (blur + rounded corner) does not count as reuse.
- Do not wrap every layout container in a glass card. Page layout stays page layout.
- A container lens must not steal clicks, typing or drags from child controls.
Tokens
CSS variables live in assets/design-system/tokens.css, the source of truth is tokens.js (node build-tokens.mjs regenerates tokens.css / tokens.json).
| Group | Values |
|---|---|
| Ink / muted / accent | #22252b / #6b7079 / #2875ed |
| Focus / page / success | #2576e8 / #f7f8fa / #236442 |
| Type | 16 body, 14 control, 12 caption; system sans (PingFang SC / Microsoft YaHei for Chinese) |
| Space | 4, 8, 16, 24, 32 px |
| Radius | 14 chrome, 34 panel, 60 lens, 999 pill |
| Surface fallback | rgba(245,250,255,.30), solid #eef2f8, inset highlight + 0 12px 24px -12px #2135524d |
| Spring | cubic-bezier(.2,.9,.25,1.22), elasticity .70; 160 / 240 / 520 ms |
| Optics | gold #e2b54e, green #52bb83, light #90e8fa; flow 1100 ms, settle 280 ms, magnification 1.8 |
| Audio | on, 28% volume, max 6 voices, 70 ms cooldown, key lens.sound.v1 |
Material presets (0–1 optics passed to setMaterial()):
| Preset | Refraction | Blur | Dispersion |
|---|---|---|---|
thick (default) | .90 | .24 | .65 |
clear | .65 | .12 | .35 |
soft | .38 | .64 | .12 |
Pastel cyan, blue and peach belong to the sampled environment, not to success/error status. Page-specific overrides go through CSS variables or setMaterial(), never by copying renderer constants.
Integration
<link rel="stylesheet" href="./design-system/tokens.css">
<link rel="stylesheet" href="./design-system/components.css">
<div id="stage" style="min-height:280px;padding:32px"></div>
<script type="module">
import {createComponent, createGlassSystem} from './design-system/index.js';
const stage = document.querySelector('#stage');
const focus = createComponent('switch', {label: 'Focus', checked: false, onChange: console.log});
stage.append(focus.element);
const system = createGlassSystem(stage); // Thick, quiet sound, no idle movement
// Optional: system.setMaterial('clear' | {refraction, blur, dispersion});
// system.setScene('grid'); // aurora | sunset | ocean | grid
// system.jelly.setIntensity(.55);
// On unmount:
// system.destroy(); focus.destroy();
</script>
- Insert the element first, then create the system. Components added later:
system.mount(element). - Destroy the system before component handles. Destroy card-list handles last: they own transferred children.
- Serve over HTTP;
file:blocks ES module imports. The package can also be installed as a local ESM package (index.d.tsships with it). - Refraction samples the system canvas, not arbitrary DOM, iframes or video. Text under glass stays real DOM so it stays readable and accessible.
- To refract your own scene, pass
artwork(ctx, {width, height, dpr, scene})at creation and callsystem.setScene(name)to redraw. Supplying artwork disables the built-in scenes.
Components
All created with createComponent(id, props) → {element, destroy, ...}. Full props and return values: assets/design-system/README.md.
| Group | IDs |
|---|---|
| Actions | button, icon-button, menu, dock |
| Choice | choices, tabs, switch, toggle-tile, drop-select |
| Input | search, slider, stepper, xy-slider |
| Structure | panel, lens, card-list, notification |
| Status | progress |
Data composites (via mountShowcaseCharts(host, {language, initialState})) | line-chart, bar-chart, ring-chart, records-table |
Notable behaviours to preserve when composing:
setValue()does not fire business callbacks by default; user interaction does. Optical controls emit their cue only with{emit:true}on commit.drop-selectsnaps to the nearest available well on release, ignores disabled wells, and animatesrest → held → travelling → merging → rest; the destination does not light up before arrival.progressrestricts continuous fibre motion to the completed region, crosses to green or gold once on completion, and never re-firesonCompletewithout a reset.card-listuses native horizontal scroll + snap, pauses on hover/focus/drag/hidden, and owns children passed as handles.- Keyboard focus is managed by
createGlassSystem(stage): visible after keyboard use, hidden on pointer movement, without blurring the real focus.
Motion
- Press: slight lift plus deformation along the movement direction. Release: spring back and stop.
- Continuous transitions start from the current pose: grab, snap, merge, withdraw, cancel. Never swap size/position by hiding a layer.
- One motion language for pointer, keyboard and programmatic changes.
prefers-reduced-motion: damp gesture feedback, disable non-essential continuous movement, no idle jiggle, no inertia, programmatic smooth scroll becomesauto.- Pause WebGL when the page is hidden or the stage leaves the viewport.
Sound
- Enhancement only; never the sole carrier of a state. Default 28%, no autoplay, no hover, no background loop.
- 13 synthesized cues:
grab,release,switchOn,switchOff,select,tick,type,success,favorite,dismiss,restore,reset,press. Paper-friction is a continuous gesture sound, not a cue. - AudioContext is created on the first user gesture. Audio failure must leave controls fully usable.
- Disabled controls make no sound. Cancelled gestures do not play completion cues. Storage failure falls back to memory.
Accessibility and degradation
- Controls keep native roles, labels, values and keyboard behaviour; focus outline is separate from selection state.
- Hit areas at least 44px high where practical; a switch's whole native button is its hit area.
- Selection state keeps text or a checkmark, never colour alone.
- Semantic colours and body text must be re-checked for contrast on the final background.
- Without WebGL2 (or after context loss): CSS glass, SVG wave lines and edge light take over. Input and keyboard stay usable; real refraction and lens magnification are not promised.
- Never claim visual verification from "no console errors". Test light, dark, grid, CSS fallback, keyboard, muted audio and reduced motion.
Extending
import {defineComponent, createComponent} from './design-system/index.js';
defineComponent('approve', ({label, onPress}) =>
createComponent('button', {label, variant: 'success', sound: 'success', onPress}));
- New visual decision → change
tokens.jsand regenerate, or override CSS variables per root. - New material → optics parameters; new semantic action → register a cue; new control → decompose into the seven elements and compose existing lens components first.
- Do not copy the renderer, gesture physics or material constants into pages. Do not override built-in component names.
- Custom cues:
system.sound.register(name, {duration, tones:[[freq, target, amp, delay]]}), max 1 s. Play from a user action callback.
Verification
The bundled package is the portable runtime, not the source lab. Available here:
node build-tokens.mjs # after editing tokens.js; regenerates tokens.css / tokens.json
Then serve the folder over HTTP and check in a real browser: pointer and keyboard on every control, 1280×900 and 390×844, light/dark/grid scenes, WebGL fallback, mute, reduced motion, and the teardown path. assets/design-system/index.html is the bilingual live catalog; examples/optics.html is the full integration page.
The source repo's lab adds npm test, npm run build and npm run check (audio PCM, throttling, storage, extensions, single-file builds). Those scripts are not bundled; do not claim them unless you run them from the original repo.
Asset map
assets/design-system/
index.js, index.d.ts, tokens.js|json|css, components.js|css
runtime.js, focus.js, sound.js, card-list.js, component-patterns.js
showcase*.js|css, internal/ (renderer, optics, chart glass, jelly)
examples/ (basic.html, optics.html, typed.ts), docs (DESIGN, SCOPE, CONTEXT, INTERACTIONS, COMPONENTS, SHOWCASE)
references/lens-start-here.md
assets/design-system/LICENSE-design-systems