PROJECT_07 // LIQUID_GLASS

LIQUID
GLASS

CSS // SVG Filter // Zero JS Frameworks

A real refraction effect built from a layer stack: SVG displacement map, blur, tint, edge shine and frost grain. No WebGL, no canvas. Drag the panel below and watch the background bend through it.

#CSS#SVG#backdrop-filter#feDisplacementMap
01

PLAYGROUND

Refraction only shows over contrast. That is why the stage below is loud on purpose: the displacement map warps whatever sits behind the glass, and over a flat background there is nothing to warp. Drag the panel around, then break it with the sliders.

GLASS_01

drag me

PRESETS
02

THE_DOCK

The same stack, shaped like the macOS dock. Hover it. The spring curve on the hover state is what sells the "liquid" part: glass that is stiff feels like plastic.

03

THE_RECIPE

1 // DISPLACEMENT

An SVG filter with feTurbulence noise feeding an feDisplacementMap, referenced from CSS via backdrop-filter: url(#id). This is the actual refraction: it warps the pixels behind the element. Rendered once per page, shared by all surfaces.

2 // BLUR + TINT

A small blur (2px, not 20) softens the warped backdrop, and a fixed dark tint with saturate(1.5) gives the material body. The tint stays the same in light and dark mode. It reads differently because the backdrop changes, not the glass.

3 // EDGE SHINE

Two inset box-shadows, bright on the top-left edge and faint on the bottom-right, plus a 1px translucent border. This fakes the specular highlight where light catches the rim of real glass.

4 // GLOSS SHEEN

A diagonal gradient on ::before, from white at 10% opacity to nothing. The giveaway of a glossy surface. Toggle it off in the playground and the panel immediately looks like a matte plastic card.

5 // FROST GRAIN

A static feTurbulence noise texture as a data URI on ::after at 6% opacity with mix-blend-mode: overlay. Rastered once, zero per-frame cost. Without it the surface is too clean to read as frosted glass.

04

THE_CODE

The whole effect is three snippets. Copy them, they are exactly what runs on this page.

the-filter.svgSVG
<svg width="0" height="0" aria-hidden="true">
  <filter id="lgDistort"
          x="0%" y="0%" width="100%" height="100%"
          color-interpolation-filters="sRGB">
    <feTurbulence type="fractalNoise"
                  baseFrequency="0.008 0.008"
                  numOctaves="2" seed="17"
                  result="noise" />
    <feGaussianBlur in="noise" stdDeviation="3"
                    result="soft" />
    <feDisplacementMap in="SourceGraphic" in2="soft"
                       scale="60"
                       xChannelSelector="R"
                       yChannelSelector="G" />
  </filter>
</svg>
the-surface.cssCSS
.glass {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 1.5rem;
  background: rgba(15, 15, 24, 0.2); /* fixed tint */
  backdrop-filter: blur(2px) url(#lgDistort) saturate(1.5);
  -webkit-backdrop-filter: blur(6px) saturate(1.5);
  box-shadow:
    inset 1px 1px 0 rgba(255, 255, 255, 0.18),
    inset -1px -1px 0 rgba(255, 255, 255, 0.05),
    0 8px 32px rgba(0, 0, 0, 0.24);
}
.glass > * { position: relative; z-index: 1; }
sheen-and-grain.cssCSS
/* diagonal gloss sheen — the glossy giveaway */
.glass::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg,
    rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.02) 40%, transparent 60%);
}

/* static frost grain: feTurbulence rastered once, zero per-frame cost */
.glass::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  opacity: 0.06; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

Two rules that make or break it: the displacement only shows over a high-contrast backdrop, and the tint stays fixed across light and dark mode. What changes between themes is the backdrop behind the glass, never the glass itself.

05

BROWSER_SUPPORT

The honest part: url() filters inside backdrop-filter are Chromium-only. Chrome, Edge, Arc and Brave render the full refraction. Safari and Firefox get a graceful fallback, the same stack minus the displacement map: blur, saturation, tint, shine and grain still hold up as a convincing frosted surface.

Performance rules that came out of building this: only one or two refractive surfaces visible at a time, never animate filter or backdrop-filter (only transform and opacity), and keep the displacement to a single map. A four-map chromatic aberration variant looked amazing and lagged catastrophically.

06

TECH_STACK

SVG Filters // feTurbulence + feDisplacementMap
backdrop-filter // blur + url() + saturate
CSS Layer Stack // tint, shine, sheen, grain
Vue 3 // reactive filter params, pointer drag
Zero Dependencies // no WebGL, no canvas, no libs