@fluidkit/blur/style.cssThe glass: lb, its variants, and the styles press and melt rely on.
Glass for the web. One class to make it, two numbers to tune it.
Surface Tension — Lumen. Backdrop blur, a fill, rims and a little light.
Install the package, import the stylesheet once, and put lb on anything.
Install.
pnpm add @fluidkit/blurImport the stylesheet, once, anywhere in your app.
import "@fluidkit/blur/style.css";Add the class.
<button class="lb">Glass</button>Install press if you use the press classes (lb-highlight, lb-swell, lb-stretch, lb-interactive). It adds one listener to the document, so elements added later work too, with any framework. On the server it does nothing.
import { installPress } from "@fluidkit/blur/press";
installPress();No build step? Link the stylesheet and load the auto entry, which installs press itself. With a bundler, import "@fluidkit/blur/auto" does the same.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fluidkit/blur@1/dist/style.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/@fluidkit/blur@1/dist/auto.js/+esm"></script>The material is plain CSS. Press and melt are optional scripts, each in its own entry: import only what you use. They work independently of each other.
@fluidkit/blur/style.cssThe glass: lb, its variants, and the styles press and melt rely on.
@fluidkit/blur/pressExports installPress. Runs highlight, swell and stretch on springs.
@fluidkit/blur/meltExports installGlassGroups, createGlassGroup and the type GlassGroup. Merges nearby glass into one surface; needs the stylesheet.
@fluidkit/blur/autoInstalls press on import. No named exports. For pages without a build step. Glass groups aren’t installed: load melt too and call installGlassGroups().
To flow a control into a panel of the same glass, use Morph. It works inside glass groups.
lb is the material. lb-clear and lb-tinted change its look. lb-highlight, lb-swell and lb-stretch react to presses, and lb-interactive turns on all three. Combine them freely.
lbThe material. Backdrop blur, a fill, volume, rims, an edge and a shadow. Required.lb-clearAt least transparency 1. For glass over photos and video, where the content is the point.lb-tintedThe fill is --lb-tint and the text is --lb-tint-text. Volume and rims stay.lb-highlightLights up where it's pressed and follows the finger. Press it.lb-swellGrows while pressed, on a spring that can turn around mid-flight. Small glass grows more, big glass barely.lb-stretchPulls toward the finger and stretches along the drag. Small glass is lively, big glass is heavy. Drag it.lb-interactiveAll three at once, the usual set for a button, chip or icon. 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" style="border-radius: 24px; padding: 16px">Card</div>The glass is built from these layers, back to front. It uses only features that every browser engine renders the same way, and no pseudo-elements, so ::before and ::after stay free for you.
All variables inherit: set one on any ancestor and every glass element inside uses it.
--lb-transparency0.50 is opaque, 1 is clear. Moves fill and blur together.--lb-depth0.50 is flat, 1 is pronounced. Moves shading, glow and rims.--lb-darkper theme0 is light, 1 is dark. Follows the system and data-theme.--lb-fill-light#ffffffFill in the light theme.--lb-fill-dark#3a3a3aFill in the dark theme.--lb-fillunsetOne fill for both themes; wins over 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.--lb-highlight-size1Reach of the press highlight, as a multiplier. It already follows the element's size.--lb-swell1.1Pressed scale at button size. Smaller glass grows more, bigger less.--lb-stretch2Stretch strength; 0 turns it off..toolbar {
--lb-transparency: 0.8;
--lb-depth: 0.3;
}
.danger {
--lb-tint: #ff3b30;
}Everything prefixed --_ is internal calibration and may change without notice.
Colors are resolved on each glass element, so they follow whichever theme applies there. Transparency and depth still control how strongly a color shows.
/* Your own neutral, per theme */
:root {
--lb-fill-light: #f5f5f7;
--lb-fill-dark: #1c1c1e;
}
/* Warm glass: fill, shading and rims all lean amber */
.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;
}Glass picks a text color that contrasts with its fill. To override it, set color on the element, or --lb-text.
The theme is one inherited variable, --lb-dark: 0 is light, 1 is 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 where it marks the dark theme:
/* Tailwind, shadcn, next-themes with attribute="class" */
.dark { --lb-dark: 1; }
/* daisyUI and other named themes */
[data-theme="dracula"] { --lb-dark: 1; }All styles are in @layer liquid-blur, so your unlayered CSS overrides them without !important. Among layers, the one declared first has the lowest priority. Tailwind v4 declares its layers when imported, so glass imported after it would override utilities such as rounded-xl. Declare the layer order once, before both imports:
@layer liquid-blur, theme, base, components, utilities;
@import "tailwindcss";
@import "@fluidkit/blur/style.css";Glass follows the system’s accessibility settings on its own; no code needed.
With the system setting on, glass turns opaque and the backdrop filter goes away.
Opaque glass with a stronger edge, so every control keeps its outline.
Windows High Contrast drops backgrounds and shadows; an outline takes their place, one around a melted group.
No swell, no stretch; morphs switch immediately. Presses still light up.
Enter and Space on a focused glass press it, until that same key comes up.
data-lb-material="solid" on any ancestor makes all glass inside it opaque.
The press classes need installPress() (see Get started). Without it, highlight and swell fall back to :active, centered on the element.
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 on release.
A soft glow under the finger. Its size grows with the element, up to a limit on large panels. --lb-highlight-size multiplies it.
Highlight and swell run on springs that can reverse mid-way. While they move, the script sets scale, the highlight strength and the press point (--lb-press-x, --lb-press-y) inline, and removes them at rest. Swell multiplies your own scale.
Larger glass swells and stretches less, moves slower and bounces less. --lb-swell and --lb-stretch adjust the strength.
Writes only transform, once per frame, without reading layout. Your own transform is kept and the stretch is added after it; your inline transform and will-change are restored at rest. Stretch sets touch-action: none and disables text selection on the element.
Releasing more than 32px outside the element cancels the click.
Don’t put a CSS transition on scale for swell elements or on transform for stretch elements: the script updates them every frame and a transition would lag behind. Use press classes on single controls, not containers: highlight lights the whole element on a press anywhere inside it, and stretch blocks scrolling and text selection in its whole subtree.
A disabled control (:disabled, or aria-disabled="true") isn’t pressed: no highlight, swell or stretch, with the script or without. How it looks disabled is up to you.
Glass elements inside an lb-group merge into one surface when they get close and separate as they move apart. The group draws one backdrop clipped to the merged outline, so overlaps aren’t blurred twice.
transition, and the glass follows it out and back, neck and all.@keyframes animation, nothing else: the drop pulls away from the pill and falls back in. With reduced motion it stays put.<div class="lb lb-group">
<div class="lb">…</div>
<button class="lb lb-interactive">…</button>
</div>
<script type="module">
import { installGlassGroups } from "@fluidkit/blur/melt";
installGlassGroups();
</script>Groups need the @fluidkit/blur/melt entry. Without it, or while the children are far apart, they render as ordinary glass.
Children start to merge when they are closer than --lb-merge, 18px by default. Set it on the group.
The group follows its children’s springs, inline style and class changes, size changes, and CSS transitions and animations. For anything else that moves them, call update().
The group uses the same variables as any glass. While it draws the shared surface, the root has data-lb-melted. An lb-tinted child keeps its tint on top.
Server rendering. The entry can be imported on the server; installGlassGroups() does nothing there. 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 the markup, so a re-render that sets the class doesn’t drop lb.
import { useEffect } from "react";
import { installGlassGroups } from "@fluidkit/blur/melt";
export function Toolbar() {
// After hydration: the group adds its own elements to the root
useEffect(() => installGlassGroups(), []);
return (
// "lb" in the markup too, so a re-render keeps it
<div className="lb lb-group">
<div className="lb">…</div>
<button className="lb lb-interactive">…</button>
</div>
);
}The functions behind press and melt. What each entry exports is listed under Entries.
installPress(root?: Document): () => void
Installs press behaviors on root (default document) and returns a function that uninstalls them. Calling it again returns the same function. While installed, <html> has data-lb-interaction. Works inside 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, if the page goes away
uninstall();installGlassGroups(root?: Document): () => void
Turns every .lb-group in the document into a group, including ones added later, and releases a group when its root leaves the document. Returns a cleanup that destroys the groups it created; calling it again returns the same cleanup. Does nothing without a DOM.
createGlassGroup(root: HTMLElement): GlassGroup
Creates a group for one 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/blur/melt";
const group = createGlassGroup(toolbar);
// A child moved by something the group can't see
group.update();
// Later
group.destroy();Any browser with backdrop-filter, color-mix() and CSS pow(). Without @property (Firefox before 128) the glass still renders, but some settings inherit into nested glass and the press highlight doesn’t fade. Groups also need clip-path: path(), mask with an inline SVG, and Canvas 2D. The JavaScript is ESM targeting ES2022.
filter, opacity below 1, mask, clip-path, mix-blend-mode or its own backdrop-filter cuts the glass off from everything behind that ancestor.update(). The merged outline uses circular arcs, even for squircle children.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 leave it unclipped. Clipping with plain border-radius is fine.Refraction on the web needs SVG filters, which only one browser engine applies to a backdrop, and they are expensive on every frame of scrolling. Liquid Blur uses only what renders the same in every browser.
No. The material is plain CSS. The optional scripts add press behaviors and glass groups.
Use the classes as they are, and install press once at the root. It returns its own cleanup.
import { useEffect } from "react";
import { installPress } from "@fluidkit/blur/press";
export function App() {
useEffect(() => installPress(), []);
return <button className="lb lb-highlight">Glass</button>;
}A backdrop filter costs GPU time in proportion to the area of glass on screen: a toolbar is cheap, a full-page panel is not. While animating, the script writes only transforms and two custom properties per frame and never reads layout.
No. It’s inspired by it, but has no lensing or refraction and uses only what browsers support today.