Liquid Blur

Glass for the web. One class to add it, two variables to tune it.

Now Playing

Surface Tension — Lumen. Backdrop blur, a fill, rims and a little light.

0.50
0.50
Material
Tint
Press
Group
Theme
Backdrop

On this page

Get started

Install the package, import the stylesheet once, and add the lb class to any element.

  1. Install.

    pnpm add @fluidkit/core
  2. Import the stylesheet once, anywhere in your app.

    import "@fluidkit/core/style.css";
  3. Add the class.

    <button class="lb">Glass</button>
  4. Set up press effects if you use the press classes (lb-highlight, lb-swell, lb-stretch, lb-interactive). It adds a single listener to the document, so elements added later work too, in any framework. On the server it does nothing.

    import { installPress } from "@fluidkit/core";
    
    installPress();
  1. Install. The React components and hooks, with all of core (React 18+).

    pnpm add @fluidkit/react
  2. Import the stylesheet once, in your app’s root (a layout or main.tsx).

    import "@fluidkit/react/style.css";
  3. Use Glass. Variants are props, and render lets you use your own element. Press props (interactive, highlight, swell, stretch) set up press effects automatically.

    import { Glass } from "@fluidkit/react";
    
    <Glass render={<nav />}>…</Glass>
    <Glass interactive render={<button type="button" />}>Follow</Glass>

    The classes are plain CSS, so className="lb lb-interactive" also works. In that case, call usePress() once in your root component.

  1. Install. The Vue components and composables, with all of core (Vue 3.5+).

    pnpm add @fluidkit/vue
  2. Import the stylesheet once, in your app’s entry (main.ts, or Nuxt’s css option).

    import "@fluidkit/vue/style.css";
  3. Use Glass. Variants are props, and as-child lets you use your own element. Press props (interactive, highlight, swell, stretch) set up press effects automatically.

    <script setup lang="ts">
    import { Glass } from "@fluidkit/vue";
    </script>
    
    <template>
      <Glass as-child><nav>…</nav></Glass>
      <Glass interactive as-child><button type="button">Follow</button></Glass>
    </template>

    The classes are plain CSS, so class="lb lb-interactive" also works. In that case, call usePress() once in your root component.

  1. Install. The Svelte components and runes, with all of core (Svelte 5.29+).

    pnpm add @fluidkit/svelte
  2. Import the stylesheet once, in your root layout (src/routes/+layout.svelte in SvelteKit) or your app’s entry.

    import "@fluidkit/svelte/style.css";
  3. Use Glass. Variants are props, and a child snippet lets you use your own element: spread the props it gives onto it. Press props (interactive, highlight, swell, stretch) set up press effects automatically.

    <script lang="ts">
      import { Glass } from "@fluidkit/svelte";
    </script>
    
    <Glass>
      {#snippet child({ props })}<nav {...props}>…</nav>{/snippet}
    </Glass>
    <Glass interactive>
      {#snippet child({ props })}<button {...props} type="button">Follow</button>{/snippet}
    </Glass>

    The classes are plain CSS, so class="lb lb-interactive" also works. In that case, call usePress() once in your root layout.

No build step? Link the stylesheet and load the auto entry, which sets up press effects for you. With a bundler, import "@fluidkit/core/auto" does the same.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fluidkit/core@2/dist/style.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/@fluidkit/core@2/dist/auto.js/+esm"></script>

Entries

The material is plain CSS. Press effects and glass groups are optional scripts: what you don’t import isn’t in your bundle.

Material @fluidkit/core/style.css

The glass: lb, its variants, and the styles the scripts rely on, with Motion’s springs and transitions. Each framework’s package has it as its own style.css.

Press installPress

Animated highlight, swell and stretch.

Melt installGlassGroups

With createGlassGroup and the GlassGroup type. Merges nearby glass into one surface. Needs the stylesheet.

Auto @fluidkit/core/auto

Sets up press effects on import, for pages without a build step. Doesn’t set up glass groups; for those, also call installGlassGroups().

@fluidkit/react provides Glass, Glass.Group and a few hooks. See Components. It sets up press effects and groups for you.

@fluidkit/vue provides Glass, Glass.Group and a few composables. See Components. It sets up press effects and groups for you.

@fluidkit/svelte provides Glass, Glass.Group and a few functions. See Components. It sets up press effects and groups for you.

To animate a button into a panel of the same glass, use Morph. It works inside glass groups.

Classes

lb is the material. lb-clear, lb-tinted and lb-lens change how it looks. lb-highlight, lb-swell and lb-stretch react to presses; lb-interactive turns on all three. lb-edge and lb-edge-x fade the edges of scrolling content (see Scroll Edge). Combine them freely.

lbThe material. Backdrop blur, a fill, volume, rims, an edge and a shadow. Required.
lb-clearClearer glass, at least transparency 1. For use over photos and video.
lb-tintedFilled with --lb-tint, text in --lb-tint-text. Volume and rims stay.
lb-lensGlass without blur, so what's underneath stays sharp. To turn a thumb into a lens while held, transition --lb-lens.
lb-highlightLights up where it's pressed and follows the finger. Press it.
lb-swellGrows while pressed, on a spring that can reverse mid-way. Small glass grows more, large glass barely.
lb-stretchPulls toward the finger and stretches along the drag. Small glass feels light, large glass heavy. Drag it.
lb-interactiveAll three at once, the usual set for buttons, chips and icons. Press it, drag it.

Set the shape with border-radius. The default is a capsule.

<button class="lb lb-interactive">Press me</button>
<div class="lb lb-highlight lb-stretch">A bar: lights and stretches, doesn't swell</div>
<button class="lb lb-tinted lb-highlight">Tinted</button>
<div class="lb lb-clear">Over a photo</div>
<div class="lb lb-lens">A lens</div>
<div class="lb" style="border-radius: 24px; padding: 16px">Card</div>

Glass adds the classes based on its props. tint also accepts a color.

<Glass interactive render={<button />}>Press me</Glass>
<Glass highlight stretch>A bar: lights and stretches, doesn't swell</Glass>
<Glass tint="#ff3b30" highlight render={<button />}>Tinted</Glass>
<Glass clear>Over a photo</Glass>
<Glass lens>A lens</Glass>
<Glass style={{ borderRadius: 24, padding: 16 }}>Card</Glass>

Glass adds the classes based on its props. tint also accepts a color.

<Glass interactive as-child><button>Press me</button></Glass>
<Glass highlight stretch>A bar: lights and stretches, doesn't swell</Glass>
<Glass tint="#ff3b30" highlight as-child><button>Tinted</button></Glass>
<Glass clear>Over a photo</Glass>
<Glass lens>A lens</Glass>
<Glass :style="{ borderRadius: '24px', padding: '16px' }">Card</Glass>

Glass adds the classes based on its props. tint also accepts a color.

<Glass interactive>
  {#snippet child({ props })}<button {...props}>Press me</button>{/snippet}
</Glass>
<Glass highlight stretch>A bar: lights and stretches, doesn't swell</Glass>
<Glass tint="#ff3b30" highlight>
  {#snippet child({ props })}<button {...props}>Tinted</button>{/snippet}
</Glass>
<Glass clear>Over a photo</Glass>
<Glass lens>A lens</Glass>
<Glass style="border-radius: 24px; padding: 16px">Card</Glass>

A lens while held

A lens is the same glass without blur, so what’s underneath shows through sharply. lb-lens makes glass a lens permanently. To switch to a lens only while something is held, animate --lb-lens from 0 to 1 instead; it’s a registered property, so it transitions, as does --lb-transparency. For example, a switch thumb that’s solid white at rest and becomes a clear lens while pressed or dragged (with Drag, which adds data-dragging), like in iOS:

.switch__thumb {
  --lb-transparency: 0;
  --lb-fill: #fff;
  transition: --lb-lens 0.3s, --lb-transparency 0.3s, scale 0.35s;
}
.switch:active .switch__thumb,
.switch__thumb[data-dragging] {
  --lb-lens: 1;
  --lb-transparency: 1;
  scale: 1.45 1.5;
}
<button class="switch" role="switch" aria-checked="true">
  <span class="switch__thumb lb"></span>
</button>

The lens doesn’t magnify; that would need refraction. With the system’s reduced transparency or increased contrast settings, it stays solid.

Layers

The glass is made of these layers, from back to front. It only uses features that render the same in every browser engine, and no pseudo-elements, so ::before and ::after are free for you.

  1. Backdrop: blur and saturation.
  2. Fill: one color (or the tint) at a calculated opacity.
  3. Volume: the surface darkens toward the edges and catches a little light under the top and bottom rims.
  4. Rims: thin highlights along the top and bottom. On a capsule they fade toward the ends.
  5. Edge: a thin dark line just outside.
  6. Drop shadow.

Customize

All variables are inherited: set one on any ancestor and all glass inside uses it.

Material
--lb-transparency0.50 is opaque, 1 is clear. Moves fill and blur together. Registered, so it transitions.
--lb-depth0.50 is flat, 1 is pronounced. Moves shading, glow and rims.
--lb-lens00 is glass, 1 is a lens without blur, like lb-lens. Registered, so it transitions.
--lb-darkper theme0 is light, 1 is dark. Follows the system and data-theme.
Color
--lb-fill-light#ffffffFill in the light theme.
--lb-fill-dark#3a3a3aFill in the dark theme.
--lb-fillunsetOne fill for both themes. Overrides the two above.
--lb-text-light#000000Text in the light theme.
--lb-text-dark#ffffffText in the dark theme.
--lb-textunsetOne text color for both themes.
--lb-tint#007affColor of lb-tinted glass.
--lb-tint-text#ffffffText on lb-tinted glass.
--lb-shade-color#000000The dark parts: volume shading, edge, drop shadow.
--lb-shine-color#ffffffThe light parts: rims, glow, press highlight.
Behavior
--lb-highlight-size1Size of the press highlight, as a multiplier. It already scales with the element.
--lb-swell1.1Scale while pressed, at button size. Smaller glass grows more, larger less.
--lb-stretch2Stretch strength. 0 turns it off.
--lb-edge-size28pxLength of the scroll edge fade (lb-edge, lb-edge-x).
--lb-edge-start--lb-edge-sizeThe same for the top or left. 0px turns that edge off.
--lb-edge-end--lb-edge-sizeThe same for the bottom or right. 0px turns that edge off.
--lb-edge-strength1How transparent content gets at a scroll edge. 1 is fully.
--lb-stretch-axisbothStretch direction: x, y or both. Use x for a switch thumb that stays in its track.
.toolbar {
  --lb-transparency: 0.8;
  --lb-depth: 0.3;
}

.danger {
  --lb-tint: #ff3b30;
}

Variables starting with --_ are internal and may change without notice.

Colors

Colors are resolved on each glass element, so they follow the theme that applies there. Transparency and depth still control how strong the color looks.

/* Your own neutral color per theme */
:root {
  --lb-fill-light: #f5f5f7;
  --lb-fill-dark: #1c1c1e;
}

/* Warm glass: amber fill, shading and rims */
.warm {
  --lb-fill-light: #ffe08a;
  --lb-shade-color: #5a2a00;
  --lb-shine-color: #fff4c0;
}

/* Same fill in both themes */
.brand {
  --lb-fill: #0a3;
}

/* A light tint needs dark text */
.warning {
  --lb-tint: #ffcc00;
  --lb-tint-text: #111;
}

Text color is picked automatically to contrast with the fill. To override it, set color on the element, or --lb-text.

Theming

The theme is a single inherited variable, --lb-dark: 0 for light, 1 for dark. By default it follows the system setting. data-theme="light" or "dark" on any element switches everything inside it.

<html data-theme="dark">
  <section data-theme="light">
    <button class="lb">Always light</button>
  </section>
</html>

To connect another theme switcher, set --lb-dark on its dark-theme selector:

/* Tailwind, shadcn, next-themes with attribute="class" */
.dark { --lb-dark: 1; }

/* daisyUI and other named themes */
[data-theme="dracula"] { --lb-dark: 1; }

Tailwind & layers

All styles are in @layer liquid-blur, so your own unlayered CSS overrides them without !important. Tailwind v4 declares its layers on import, and the first declared layer has the lowest priority, so glass imported after Tailwind would override utilities like rounded-xl. Declare the layer order once, before both imports:

@layer liquid-blur, theme, base, components, utilities;
@import "tailwindcss";
@import "@fluidkit/core/style.css";

Accessibility

Glass follows the system accessibility settings automatically. No code needed.

  • Reduced transparency

    With the system setting on, glass becomes opaque and the blur is removed.

  • Increased contrast

    Opaque glass with a stronger edge, so every control keeps its outline.

  • Forced colors

    Windows High Contrast removes backgrounds and shadows; an outline replaces them, including around merged groups.

  • Reduced motion

    No swell or stretch, and morphs switch instantly. Presses still light up.

  • Keyboard

    Enter and Space press focused glass until the key is released.

  • Solid material

    data-lb-material="solid" on any ancestor makes all glass inside it opaque.

Press effects

The press classes need installPress() (see Get started). Without it, highlight and swell fall back to :active, centered on the element.

Glass with a press prop sets this up automatically. If you write the classes by hand, call usePress() in your root component.

Glass with a press prop sets this up automatically. If you write the classes by hand, call usePress() in your root component.

Glass with a press prop sets this up automatically. If you write the classes by hand, call usePress() in your root layout.

Pressed state

While pressed, the element has data-lb-pressed, which you can style. On touch it appears after a short delay, so starting a scroll doesn’t trigger it. A quick tap still flashes briefly.

Highlight

A soft glow under the finger. It grows with the element, up to a limit on large panels. --lb-highlight-size scales it.

Springs

Highlight and swell run on springs that can reverse mid-way. While animating, the script sets scale, the highlight strength and the press position (--lb-press-x, --lb-press-y) inline, and removes them when done. Swell multiplies your own scale.

Size

Larger glass swells and stretches less, and moves slower. --lb-swell and --lb-stretch set the strength, and --lb-stretch-axis (x, y or both) the direction. A switch thumb, for example, should only stretch along its track.

Stretch

Only writes transform, once per frame, without reading layout. Your own transform is kept, and your inline transform and will-change are restored afterwards. Stretch sets touch-action: none and disables text selection on the element.

Release outside

Releasing more than 32px outside the element cancels the click.

Don’t add a CSS transition on scale (for swell) or transform (for stretch): the script updates them every frame, and a transition would lag. Use press classes on individual controls, not containers: highlight lights up the whole element, and stretch blocks scrolling and text selection inside it.

Disabled controls (:disabled or aria-disabled="true") get no press effects. Their disabled look is up to you.

Glass groups

Glass elements inside lb-group merge into one surface when they get close, and separate as they move apart. The group draws a single backdrop for the merged shape, so overlaps aren’t blurred twice.

Split and dockPress the search button. It moves with a plain CSS transition, and the glass follows it out and back, neck and all.
Pull itPress and drag any piece. Stretch and swell bend the outline between them as they go.
DropsA CSS @keyframes animation, nothing else: the drop pulls away from the pill and falls back in. With reduced motion it stays put.
Liquid
<div class="lb lb-group">
  <div class="lb">…</div>
  <button class="lb lb-interactive">…</button>
</div>

<script type="module">
  import { installGlassGroups } from "@fluidkit/core";

  installGlassGroups();
</script>
import { Glass } from "@fluidkit/react";

<Glass.Group className="toolbar">
  <Glass>…</Glass>
  <Glass interactive render={<button />}>…</Glass>
</Glass.Group>
<script setup lang="ts">
import { Glass } from "@fluidkit/vue";
</script>

<template>
  <Glass.Group class="toolbar">
    <Glass>…</Glass>
    <Glass interactive as-child><button>…</button></Glass>
  </Glass.Group>
</template>
<script lang="ts">
  import { Glass } from "@fluidkit/svelte";
</script>

<Glass.Group class="toolbar">
  <Glass>…</Glass>
  <Glass interactive>
    {#snippet child({ props })}<button {...props}>…</button>{/snippet}
  </Glass>
</Glass.Group>
Optional script

Groups need installGlassGroups(); the components set them up for you. Without it, or while the children are far apart, they look like ordinary glass.

Merge distance

Children start merging when they’re closer than --lb-merge (default 18px). Set it on the group.

Tracking movement

The group follows its children’s springs, inline style and class changes, size changes, and CSS transitions and animations. If something else moves them, call update().

Same material

The group uses the same variables as any glass. While it draws the merged surface, the root has data-lb-melted. An lb-tinted child keeps its tint.

Server rendering. The entry is safe to import on the server, where installGlassGroups() does nothing. In React, Vue or Svelte, call it after hydration (in an effect), because the group adds its own elements to the root. Write both lb lb-group in your markup, so a re-render doesn’t drop lb.

import { useEffect } from "react";
import { installGlassGroups } from "@fluidkit/core";

export function Toolbar() {
  // After hydration, since the group adds its own elements to the root
  useEffect(() => installGlassGroups(), []);
  return (
    // Include "lb" in the markup so a re-render keeps it
    <div className="lb lb-group">
      <div className="lb">…</div>
      <button className="lb lb-interactive">…</button>
    </div>
  );
}

Server rendering. Glass.Group renders lb lb-group on the server and sets up the group after hydration. It cleans up on unmount. The group adds its own elements to the root, so select children by class (.toolbar > .lb), not with > *.

Server rendering. Glass.Group renders lb lb-group on the server, including in Nuxt, and sets up the group after hydration. It cleans up on unmount. The group adds its own elements to the root, so select children by class (.toolbar > .lb), not with > *.

Server rendering. Glass.Group renders lb lb-group on the server, including in SvelteKit, and sets up the group after hydration. It cleans up when it’s removed. The group adds its own elements to the root, so select children by class (.toolbar > .lb), not with > *.

Scroll edges

Scrolling content can fade out at its edges, like under iOS bars: add lb-edge or lb-edge-x to the scrolling element. They come with the stylesheet and don’t need glass. See Scroll Edge.

ComponentsReactVueSvelte

Glass renders a div, or your own element with render (as in Base UI). Your classes and styles are kept.

Prop Description
clear?: boolean Clearer glass for photos and video (lb-clear)
tint?: boolean | string Fill with a color. Pass a color, or true to use the inherited --lb-tint (lb-tinted)
lens?: boolean Glass without blur (lb-lens)
transparency?: number 0 opaque, 0.5 default, 1 clear (--lb-transparency)
depth?: number 0 flat, 0.5 default, 1 pronounced (--lb-depth)
interactive?: boolean Highlight, swell and stretch (lb-interactive)
highlight?, swell?: boolean Individual press effects (lb-highlight, lb-swell)
stretch?: boolean | "x" | "y" Stretch, optionally along one axis (lb-stretch, --lb-stretch-axis)

Press props set up press effects automatically, including inside iframes.

Glass.Group renders a div with lb lb-group, or your own element, and turns it into a glass group. merge sets how close children get before merging, in px (default 18). transparency and depth work like on Glass and apply to all children.

<Glass.Group className="toolbar" merge={24}>
  <Glass interactive render={<button aria-label="Back" />}><BackIcon /></Glass>
  <Glass interactive render={<button aria-label="Share" />}><ShareIcon /></Glass>
</Glass.Group>

Hooks and helpers.

  • glass(variants) returns { className, style } for the same props, for elements that aren’t Glass, like a router Link.
  • usePress() sets up press effects, if you write the press classes by hand.
  • useGlassGroup() returns a ref for a group root (with lb lb-group) and the group itself, so you can call update().
import { glass, useGlassGroup, usePress } from "@fluidkit/react";

const { className, style } = glass({ interactive: true, tint: "#0a84ff" });
<Link className={className} style={style} href="/follow">Follow</Link>;

const { root, group } = useGlassGroup();
<div ref={root} className="lb lb-group">…</div>;

Glass renders a div, or with as-child the element in its slot. Your classes and styles are kept.

Prop Description
clear?: boolean Clearer glass for photos and video (lb-clear)
tint?: boolean | string Fill with a color. Pass a color, or tint alone to use the inherited --lb-tint (lb-tinted)
lens?: boolean Glass without blur (lb-lens)
transparency?: number 0 opaque, 0.5 default, 1 clear (--lb-transparency)
depth?: number 0 flat, 0.5 default, 1 pronounced (--lb-depth)
interactive?: boolean Highlight, swell and stretch (lb-interactive)
highlight?, swell?: boolean Individual press effects (lb-highlight, lb-swell)
stretch?: boolean | "x" | "y" Stretch, optionally along one axis (lb-stretch, --lb-stretch-axis)

Press props set up press effects automatically, including inside iframes.

Glass.Group renders a div with lb lb-group, or the element in its slot, and turns it into a glass group. merge sets how close children get before merging, in px (default 18). transparency and depth work like on Glass and apply to all children.

<Glass.Group class="toolbar" :merge="24">
  <Glass interactive as-child><button aria-label="Back"><BackIcon /></button></Glass>
  <Glass interactive as-child><button aria-label="Share"><ShareIcon /></button></Glass>
</Glass.Group>

Composables and helpers.

  • glass(variants) returns { class, style } for the same props, to bind to elements that aren’t Glass: v-bind="glass({ interactive: true })".
  • usePress() sets up press effects, if you write the press classes by hand.
  • useGlassGroup() returns a function ref for a group root (with lb lb-group) and the group itself, so you can call update().
<script setup lang="ts">
import { glass, useGlassGroup } from "@fluidkit/vue";

const { root, group } = useGlassGroup();
</script>

<template>
  <RouterLink v-bind="glass({ interactive: true, tint: '#0a84ff' })" to="/follow">Follow</RouterLink>
  <div :ref="root" class="lb lb-group">…</div>
</template>

Glass renders a div, or with a child snippet an element of yours. Your classes and styles are kept.

Prop Description
clear?: boolean Clearer glass for photos and video (lb-clear)
tint?: boolean | string Fill with a color. Pass a color, or tint alone to use the inherited --lb-tint (lb-tinted)
lens?: boolean Glass without blur (lb-lens)
transparency?: number 0 opaque, 0.5 default, 1 clear (--lb-transparency)
depth?: number 0 flat, 0.5 default, 1 pronounced (--lb-depth)
interactive?: boolean Highlight, swell and stretch (lb-interactive)
highlight?, swell?: boolean Individual press effects (lb-highlight, lb-swell)
stretch?: boolean | "x" | "y" Stretch, optionally along one axis (lb-stretch, --lb-stretch-axis)

Press props set up press effects automatically, including inside iframes.

Glass.Group renders a div with lb lb-group, or an element of yours with a child snippet, and turns it into a glass group. merge sets how close children get before merging, in px (default 18). transparency and depth work like on Glass and apply to all children.

<Glass.Group class="toolbar" merge={24}>
  <Glass interactive>
    {#snippet child({ props })}<button {...props} aria-label="Back"><BackIcon /></button>{/snippet}
  </Glass>
  <Glass interactive>
    {#snippet child({ props })}<button {...props} aria-label="Share"><ShareIcon /></button>{/snippet}
  </Glass>
</Glass.Group>

Functions and helpers.

  • glass(variants) returns { class, style } for the same props, to spread onto elements that aren’t Glass: <a {...glass({ interactive: true })} href="/follow">.
  • usePress() sets up press effects, if you write the press classes by hand.
  • useGlassGroup() returns an attachment for a group root (with lb lb-group) and the group itself, so you can call update().
<script lang="ts">
  import { glass, useGlassGroup } from "@fluidkit/svelte";

  const toolbar = useGlassGroup();
</script>

<a {...glass({ interactive: true, tint: "#0a84ff" })} href="/follow">Follow</a>
<div {@attach toolbar.root} class="lb lb-group">…</div>

API

The functions behind press effects and groups. See Entries for where each one comes from.

installPress(root?: Document): () => void
Sets up press effects on root (default: document) and returns a function that removes them. Calling it again returns the same function. While active, <html> has data-lb-interaction. Works in open shadow roots; for an iframe, pass its contentDocument. Does nothing without a DOM.

const uninstall = installPress();

// An iframe has its own document
installPress(frame.contentDocument);

// Later, to remove it
uninstall();

installGlassGroups(root?: Document): () => void
Turns every .lb-group in the document into a group, including ones added later, and cleans up groups whose root is removed. Returns a cleanup function; calling it again returns the same one. Does nothing without a DOM.

createGlassGroup(root: HTMLElement): GlassGroup
Creates a group for a single element you manage yourself. Adds lb and lb-group to root if missing. Calling it again for the same root returns the existing group.

import { createGlassGroup } from "@fluidkit/core";

const group = createGlassGroup(toolbar);

// After moving a child in a way the group can't detect
group.update();

// Later
group.destroy();

Browser support

Any browser with backdrop-filter, color-mix() and CSS pow(). Without @property (Firefox before 128) the glass still renders, but some settings leak into nested glass and the press highlight doesn’t fade. Groups also need clip-path: path(), mask with inline SVG, and Canvas 2D. The JavaScript is ESM targeting ES2022. Scroll edges need scroll-driven animations (Chrome 115, Safari 26); elsewhere there’s no fade.

Limitations

  • Backdrop roots. Glass only blurs what’s inside its backdrop root. An ancestor with filter, opacity below 1, mask, clip-path, mix-blend-mode or its own backdrop-filter hides everything behind that ancestor from the glass.
  • Transforms on groups. Scaling a group or its ancestors works; rotating doesn’t. Movement the group can’t detect needs update(). The merged shape uses circular corners, even for squircle children.
  • Clipping around a group. In Chromium, an ancestor with overflow: hidden (or clip, or paint containment) and a corner-shape other than round shows a blurred rectangle around merged glass. Put the shape on the ancestor’s background instead and don’t clip. Clipping with plain border-radius is fine.
  • No refraction. See the FAQ. A lens doesn’t magnify.
  • Lenses in groups. While merged, a group draws its children’s glass as one surface, lenses included. Don’t put lens glass directly in a group.

FAQ

Why no refraction?

Refraction on the web needs SVG filters, which only one browser engine applies to a backdrop, and they’re expensive on every frame while scrolling. Liquid Blur only uses what renders the same in every browser.

Does it need JavaScript?

No. The material is plain CSS. The optional scripts add press effects and glass groups.

How do I use it with React, Vue or Svelte?

In React, Vue and Svelte, use Glass (see Components with your framework chosen). Elsewhere, use the classes and set up press effects once in your root component; installPress() returns its own cleanup.

import { useEffect } from "react";
import { installPress } from "@fluidkit/core";

export function App() {
  useEffect(() => installPress(), []);
  return <button className="lb lb-highlight">Glass</button>;
}
How fast is it?

A backdrop filter costs GPU time proportional to the area of glass on screen: a toolbar is cheap, a full-page panel isn’t. While animating, the scripts only write transforms and two CSS variables per frame and never read layout.

Is this Apple's Liquid Glass?

No. It’s inspired by it, but has no refraction and only uses what browsers support today.

PlaygroundIts hard cases, live: slowed down, on other backdrops, with their boxes drawn.