npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

caustic

v0.1.0-beta.1

Published

Physically-based liquid glass for the web. Real refraction from Snell's law, rendered in a shader. Zero dependencies.

Readme

caustic

Physically-based liquid glass for the web. Refraction computed per pixel from Snell's law against a real height profile, rendered in a shader.

Not a blur. A blur scatters light, which is what frost does; a lens bends it, so edges behind it compress toward the rim, straight lines curve, and the highlight travels as the surface turns. No blur radius produces that at any strength.

Zero runtime dependencies. ESM. MIT.

npm install caustic@beta

Beta. The optics and the shader are settled and covered by tests; the control props are newer and may still move before 1.0. The @beta tag is required: nothing is published to latest yet, so a bare npm install caustic will not resolve.

The one thing to know first

Glass has to be handed the picture it bends.

A shader samples a texture, and the document's own pixels are not one. There is no API that hands you the rendered page as an image, and there should not be, since that would be a read of everything on a user's screen. So a pane cannot see what is behind it in the DOM sense, and no amount of API design changes that.

Every component here therefore takes a backdrop, paints one, or sits on a surface that does. It reads like a worse API than floating over a backdrop and it is the opposite: the same information written down instead of guessed at.

import { Glass } from "caustic/react";

<Glass backdrop="/photo.jpg" radius={28} style={{ width: 420, height: 240 }}>
  <h2>Anything you like</h2>
</Glass>;

backdrop is refracted. children are not: they sit on the surface, unrefracted, which is the only reason a label on glass is readable.

Which component

| | | |---|---| | <Glass> | A pane with your own content on it. The general case. | | <Loupe> | A circular lens you drag across a surface. | | <Switch> <Slider> <ColorSlider> <Button> <Toolbar> | Finished controls, one material. | | <Blob> | A fixed two-lobed shape, for looking at. | | <MediaSurface> + <MediaGlass> | The primitive underneath all of them. |

<Glass> or a surface

The rule that decides it:

Use <Glass> when the glass is the whole box. Use a <MediaSurface> when the glass sits on something larger.

<Glass> owns its backdrop and covers the picture to its own box. That is right for a card that fills its own frame. It is wrong for a button on a photograph: a 168×52 pill cover-cropping a landscape shows a zoomed slice unrelated to the picture behind it. A pane that is small against its backdrop needs the backdrop to be a shared thing it is a window into, which is what a surface is.

import { MediaSurface, MediaGlass } from "caustic/react";

const photo = useRef<HTMLImageElement>(null);

<MediaSurface source={photo} objectFit="cover" live={false}>
  <img ref={photo} src="/photo.jpg" alt="" style={{ display: "block", width: "100%" }} />
  <MediaGlass radius="pill" style={{ width: 168, height: 52 }}>Continue</MediaGlass>
</MediaSurface>;

One surface is one WebGL context, and a browser keeps roughly eight to sixteen before silently dropping the oldest. Put many panes on one surface rather than giving each its own.

The rule the library is arranged around

Moving a pane must never regenerate its displacement map.

The map is a function of the pane's shape, not of where it is, so a move costs a viewport and a uniform. That is what makes <Loupe> draggable at 60fps. Optics changes take the expensive path and rebuild the map; position changes never do.

Four things that will cost you an afternoon

Each of these renders something plausible and wrong, and none of them errors.

Keep the surface's source in normal flow. The panes are drawn onto the surface's own absolutely positioned canvas. An absolutely positioned <img> or <canvas> source paints above that canvas and hides every pane on it. The glass renders perfectly and you see none of it, which looks exactly like optics that do not work.

Never transform an element that owns a pane. The glass is drawn into the surface's canvas at the rectangle the pane reports, so a CSS scale moves the label and the border and leaves the refraction where it was: the control comes apart into two pieces. Deform the pane through PanePlacement.squash instead.

Draw one rim, not two. MediaGlass lights its own edge from the rim term. An inset box-shadow on top of a pane that already has a strong rim puts a second contour a pixel outside the first, which reads as two layers of glass. Use one or the other.

A lens only shows what it bends. A pane over a flat sky or a wall of uniform forest refracts correctly and renders as an empty rectangle. Aim it at something with an edge in it before concluding the optics are broken.

Tiers

Three, and the library tells you which one a page is on. Only the first renders today; the other two are detected and warned about, and nothing yet paints them.

| Tier | Condition | Result | |---|---|---| | 1 | WebGL | refraction, specular, rim | | 2 | prefers-reduced-transparency | opaque fill, no blur | | 3 | no WebGL, or a tainted source | backdrop-filter: blur(), rim |

Requirements

WebGL. React 18 or 19 for caustic/react; the core entry pulls in no React at all, and nothing under its maths or map directories touches the DOM, so the optics can be tested without a browser.

Development warnings

In development the library warns about three failures that produce no error of their own: a surface whose source is positioned over the panes and hides them, a pane larger than the displacement map generated for it, and a page running below the top tier. All of it compiles out of production builds.

There were three more, covering opacity flattening, bordered clips and video under a CSS filter. Every one of them described a way the filter backend broke, and they were removed with it rather than left to warn about a code path that no longer exists.