Vaso

Move to light it · tap to ripple

Liquid Glass Effect for React

Examples

Preview: not in the package yet

Liquid search · matches light up with glass that flows between them
No. 07The Surface TensionAutumn 2026

Water doesn't like edges, and neither should glass.

Bring two drops of water close and the surface between them thins, bends and gives way. The new glass behaves the same: a button peels off its bar, and melts back in.

Light bends where glass is curved and passes straight where it is flat. That is all a lens is: a little water, a little light, and an edge that refuses to be sharp.

Under the glass is still the page itself — live, selectable text, bent by a few hundred lines of optics.

Search the article

Each match gets a <Vaso>, and one <VasoGroup> unions them, so neighbours melt together. Positions are plain CSS transforms with a transition; the group follows its members every frame. The bar is two <Vaso> trading widths the same way.

Code
search.tsx
import { Vaso, VasoGroup } from 'vaso'

// One glass per match; the group merges the ones that touch
<VasoGroup smooth={14} depth={0.9} style={{ position: 'absolute', inset: 0 }}>
  {matches.map((m) => (
    <Vaso
      key={m.key}
      style={{
        position: 'absolute',
        width: m.width,
        height: m.height,
        borderRadius: 999,
        transform: `translate(${m.x}px, ${m.y}px)`,
        transition: 'transform 0.5s, width 0.4s',
      }}
    />
  ))}
</VasoGroup>

// The bar: animate the two widths in CSS, the neck between them is free
<VasoGroup smooth={34} depth={1.2} light="pointer">
  <Vaso className="toolbar" style={{ position: 'absolute' }} />
  <Vaso className="search" style={{ position: 'absolute' }} />
</VasoGroup>
Crystal balls
Huozhi
Alpine lake
Forest
Desert
Ocean cliffs
Waterfall
Aurora
Autumn stream

Each ball is a round <Vaso> with a strong depth. A small physics loop moves the balls with transforms, and on phones the tilt feeds gravity.

Code
ball.tsx
import { Vaso } from 'vaso'

<div style={{ transform: `translate(${x}px, ${y}px) rotate(${angle}rad)` }}>
  <img src={photo} />
  <Vaso
    radius={size / 2}
    depth={5.5}
    dispersion={0.9}
    specular={0.9}
    style={{ position: 'absolute', inset: 0, borderRadius: '50%' }}
  />
</div>
Liquid touch · press and drag
Surface TensionThe Refractions
Press and drag

pressable swells the glass and forms a small lens under the finger; ripple sends a ring through it from every tap; light="pointer" turns the rim towards the cursor. The drag is an ordinary transform on a wrapper.

Code
player.tsx
import { Vaso } from 'vaso'

<div style={{ transform: `translate(${dx}px, ${dy}px)` }}>
  <Vaso pressable ripple light="pointer" radius={44} depth={1.4} blur={1.2}>
    <Player />
  </Vaso>
</div>
Frosted glass · toggle frost on every glass
--:--
Frosted

One toggle drives every glass in the card between clear and frosted: blur goes up, depth comes down, the rim gets brighter.

Code
card.tsx
import { Vaso } from 'vaso'

// frost animates from 0 (clear) to 1 (frosted)
const glass = {
  depth: 2.4 - frost * 1.2,
  blur: 0.3 + frost * 5.7,
  specular: 0.5 + frost * 0.4,
}

<Vaso radius={24} {...glass}>
  <Clock />
</Vaso>
MaterialsPreview · same optics, different matter
Watern 1.33 · round dome
Crystaln 2.1 · cut bevel
Icefrosted · turbulent
Bubblethin film · iridescent
Drag a lens

Preview. Each lens comes from the lab engine: the index of refraction and the surface profile shape the displacement map, ice mixes in turbulence and the bubble gets a thin-film rim. A material prop is next; until then, depth, blur and dispersion get you close.

Code
lens.tsx
import { Vaso } from 'vaso'

// Closest today: shape the lens with depth, blur and dispersion
<Vaso width={160} height={160} radius={80} depth={2.6} dispersion={1.4} /> // crystal
<Vaso width={160} height={160} radius={80} depth={1.2} blur={4} />         // ice
<Vaso width={160} height={160} radius={80} depth={0.8} dispersion={0.2} /> // water
Adaptive inkPreview · the bars pick their ink from what scrolls beneath
Dunes at noon
Night mode

Dark page, light ink.

Alpine lake
Under the canopy
Paper white

Bright page, dark ink.

Aurora
Waterfall
Hysteresis

No flicker on busy photos.

Ocean cliffs
Dunes at noon
Night mode

Dark page, light ink.

Alpine lake
Under the canopy
Paper white

Bright page, dark ink.

Aurora
Waterfall
Hysteresis

No flicker on busy photos.

Ocean cliffs
‹Library⋯
Scroll the feed

The scroll thumb is a plain <Vaso>. The bars read the lightness of what scrolls under them and flip their ink with two thresholds, so a busy photo never makes them flicker. The bars themselves are a preview.

Code
scrollbar.tsx
import { Vaso } from 'vaso'

// Two thresholds: flip only on a clear change
if (ink === 'dark' && lightness < 0.42) ink = 'light'
else if (ink === 'light' && lightness > 0.6) ink = 'dark'

<div style={{ position: 'absolute', right: 5, transform: `translateY(${y}px)` }}>
  <Vaso radius={10} depth={1.6} blur={0.2}>
    <div className="thumb" data-ink={ink} />
  </Vaso>
</div>
Segmented control · the pill bounces into place

The pill is one <Vaso> moved by an underdamped spring. While it travels it stretches toward its target and refracts a little harder, then bounces into place.

Code
segmented.tsx
import { Vaso } from 'vaso'

<Vaso
  width={pill.width + stretch}
  height={36}
  radius={18}
  depth={1.4 + stretch * 0.04}
  style={{ position: 'absolute', transform: `translateX(${x}px)` }}
/>
Resizable glass · drag the handle
Quick start:
import { Vaso } from 'vaso'
Vaso was blessed by the SVG filter and became a powerful glass.

Width is just a prop. While it keeps changing, vaso stretches the last displacement map instead of drawing a new one, and draws a sharp one once the size settles.

Code
resizable.tsx
import { Vaso } from 'vaso'

<Vaso width={width} height={60} depth={0.6} blur={0.6} className="rounded-full" />
Color picker · drag the lens
#ffba66

A concave lens: negative depth pulls the surrounding colours in instead of magnifying them, so the wheel twists visibly around the rim.

Code
picker.tsx
import { Vaso } from 'vaso'

<Vaso width={60} height={60} radius={30} depth={-5} dispersion={1.5} />
Notification
Design review
Starts in 10 minutes

A plain <Vaso> with some blur and a bright rim, slid in and out with a CSS transition. The text stays crisp on top of the glass.

Code
toast.tsx
import { Vaso } from 'vaso'

<Vaso radius={18} depth={1.6} blur={1.5} specular={1}>
  <div className="px-4 py-3">
    <strong>Build passed</strong>
    <p>Deployed to production</p>
  </div>
</Vaso>
Outset · how far the glass reaches past the letter
i
6
i
12
i
{ x: 20, y: 14 }
i
{ x: 28, y: 14, top: 32 }

outset grows the glass past the element without moving anything around it. A number applies to every side; sides win over axes.

Code
outset.tsx
import { Vaso } from 'vaso'

<span style={{ position: 'relative' }}>
  i
  <Vaso
    outset={{ x: 20, y: 14, top: 32 }}
    radius={12}
    depth={1.6}
    style={{ position: 'absolute', inset: 0 }}
  />
</span>

Installation

npm install vaso

Usage

Import the <Vaso> component in your React application and wrap it around any content you want to apply the glass effect to.

Tune it with`depth``blur``dispersion``radius``specular`
toolbar.tsx
import { Vaso } from 'vaso'

export function Toolbar() {
  return (
    <Vaso radius={24} depth={1.2} blur={0.5} dispersion={0.6} outset={{ x: 8, y: 4 }}>
      <button>Share</button>
    </Vaso>
  )
}