Open source · MIT

Glass controls for React, drawn by one shader.

A component kit where every button, switch and slider is a refracting WebGL lens with spring physics, light and dark themes, and no runtime beyond three.js.

Every control, live

Each control is drawn by the WebGL glass shader: clear lenses that bend what's beneath at the rim, swell when pressed and settle back into flat pills. Press, drag and toggle; all of it is live.

Buttons

Clear glass; swells on press.

Segmented control

Hold and drag the frosted pill to turn it into a magnifying lens. The theme segments switch the page: sun, system, moon.

Toggles

Knobs lift into clear glass while held.

Sliders

Glass thumbs over the accent fill; press and drag to swell the lens.

40

Glass cards

A weather card and a calendar card. The rims bend the colour sliding behind them.

How the glass works

Every control hands one lens to a shared WebGL shader. Drag the lens below and flip the render loop: both loops draw the identical image, but only one of them is free while nothing moves.

one control · live
shader passes / s
0
main thread ms / s
0.0
Idle

Renders only while something moves

The springs are stepped every frame, but the shader runs only when a spring is still moving or a prop changed. At rest the control costs nothing.

canvas2d · beneathlens · position, size, bevel, refraction, frostcanvas2d · aboveWebGL2 · TSL shaderone renderer for the whole pagedamped springs · per frame<canvas> per controlidle · renders only while something moves
  • One renderer for the page

    A single WebGL2 renderer (three.js WebGPURenderer on its WebGL backend, shaders in TSL) draws every control and copies the result into each control’s own canvas, so a page full of glass never hits the browser’s context limit.

  • Two layers and one lens

    A control paints two Canvas2D layers, what sits beneath the glass and what rides on top, and hands the shader one lens: position, size, bevel, refraction, frost.

  • Springs stepped per frame

    Motion is damped springs; a view only renders while something moves or a prop changed. prefers-reduced-motion snaps every spring to its target.

  • Prerendered, hydrated lazily

    Every page is prerendered to static HTML and the renderer loads on the client after hydration, so first paint is plain markup.

Run it, fork it, ship it

TanStack Start, Vite, Tailwind v4 and three.js. Clone it, run it with pnpm, copy the controls you need.

terminal
git clone https://github.com/bilalgursen/liquid-glass.git
cd liquid-glass
pnpm install
pnpm dev      # Vite dev server
pnpm build    # static prerender + verify-static-build
app.tsx
import { Switch } from '#/components/ui/Switch'
import { RangeSlider } from '#/components/ui/RangeSlider'

<Switch label="Notifications" checked onChange={setOn} />
<RangeSlider label="Brightness" value={40} endIcon="sun" />

// Any subtree can pick its own theme:
<div data-theme="dark">…</div>

Where things live

src/components/ui/
The kit: Button, Switch, Checkbox, RadioGroup, SegmentedControl, RangeSlider, Progress, TextField, Card, Icon.
src/components/ui/glass/
Shared glass engine: runtime.ts (WebGL renderer + TSL shader), physics.ts (springs, lens morph), draw.ts, tokens.ts.
src/components/ui/LiquidGlass.tsx
A bare glass pane you can put behind anything.
src/components/liquid-glass-slider/
The original hero slider the kit was ported from.
src/components/site/
The sections of this page and the shared site chrome.
src/components/shadcn/
shadcn/ui primitives used by the site chrome (button, badge, separator).