Liquid 3D hero sections for React, one component. Any object — text, an SVG, a logo, a primitive, a .glb — rendered as a living chrome, glass or molten surface that reacts to the cursor.
import { LiquidHero } from "liquidforge"
<LiquidHero object={{ type: "text", value: "SHIP IT" }} preset="mercury-3" />Sibling of glyphforge. Same philosophy, same repo shape, different surface treatment: glyphforge turns geometry into ASCII, liquidforge turns it into liquid metal.
packages/liquidforge/ The npm package — components, material system, forge, CLI
packages/liquidforge-mcp/ The MCP server — the same brains, for coding agents
apps/studio/ The site — landing, Studio, presets, community
| Library | React components, the GLSL material, five object generators, asset search, glTF export |
| CLI | npx liquidforge init to scaffold, npx liquidforge add to eject the source |
| MCP server | Teaches an agent the library, recommends a colourway for the site it's looking at, and finds the model. Not on npm yet — run it from this checkout, see its README |
/studio |
Forge an object, tune the material live, copy the component |
/presets |
The collection gallery — twelve families, 108 colourways, each a live render |
/how |
Four switches that break the effect on purpose, so you can see what each one was buying |
| Loop export | The Studio records a seamless 4-second loop at up to 4K — the drawing buffer is resized for the take, so the export is not limited to how big the preview happens to be |
/showcase |
Five complete demo sites you can open and use, plus a layout explorer for the eight placements |
/assets |
Search five open 3D catalogues and send a model straight into the Studio |
| Surprise me | The Studio's model tab rolls one at random out of 46,871, with its licence |
/community |
Live gallery, backed by a real database — post from the Studio's export |
npm install
npm run devStarts the Studio on localhost:3000. No build step first — the Studio resolves liquidforge to the library's source, so editing the library hot-reloads like any other file in the app.
Other scripts:
npm run build # build the publishable package (dist/)
npm run build:all # package + MCP server
npm run typecheck # typecheck every workspace
npm run mcp # build and run the MCP server over stdioEveryone on / shares one surface: their cursors are drawn on it, and their
movement ripples it. Server-sent events with an in-memory hub — POST your
position, listen on a stream for everybody else's — which needs no websocket
server, no upgrade handshake and no extra service.
Positions are normalised to the element, not the viewport, so two people on different screens point at the same part of the object rather than the same pixel. State is deliberately in memory and lossy: a cursor is worth nothing a second after it was sent. On more than one instance people would only see the cursors sharing their instance — a real limit, and swapping the hub for a Redis channel is a change to one file.
Posts go through app/api/community. With no configuration they land in a JSON
file under apps/studio/.data/, so a fresh clone has a working gallery you can
post to. For a deployment:
DATABASE_URL=postgres://… # any Postgres; the driver is loaded lazily
SUBMISSION_SALT=… # salts the hashed submitter key
MODERATION_TOKEN=… # required before the moderation route answersPosts go up straight away, the way they do everywhere people actually post.
Holding them for review is safer and it is also the reason nobody bothers, so
moderation is a takedown tool instead of a gate: GET /api/community/moderate
lists what is up and PATCH with { id, status } hides it, both behind
MODERATION_TOKEN — and with no token set the route refuses everything, because
the failure mode for a missing secret has to be closed rather than open.
A row is about 250 bytes: no thumbnails are stored, because /api/og draws the
colourway on demand and a colourway edited later then has a correct preview
without a migration.
Submissions are validated field by field rather than passed through, capped in length, restricted to presets that exist, and rate limited to three an hour per address. The address itself is never stored, only a salted hash of it.
There is no static export any more. Every route still prerenders, but
output: "export" forbids API routes, and a gallery that cannot accept a post
is not the thing that was being asked for.
WebGL2, not WebGPU. WebGPU would allow a compute-shader fluid simulation, which would give genuinely better ripples, but it excludes Safari below 18 and older Android. For a component whose entire job is to be dropped into other people's landing pages, that trade is the wrong way round. The analytic ripple field is a trail of expanding wavelets rather than a real height-field sim; it holds up, and a WebGPU path can be added later behind the same props.
Two peer dependencies. react and three. The reference implementation was plain three.js and the render loop needs to own frame timing, pointer probing and trail emission — all awkward through a reconciler — so this is not built on react-three-fiber. The environment is computed analytically in the fragment shader rather than baked through PMREMGenerator, which keeps the package free of binary assets and lets each colourway tint its own studio.
A preset is data. A palette and about twenty numbers, with no code in it. That is what lets the Studio edit one with sliders, the MCP server hand one to an agent as JSON, and codegen inline one into a component a person can still read.
Each was hit, diagnosed and fixed once. None of them throws; the surface just looks wrong. They're documented at length in the package README and in the MCP server's shader and blend topics.
- Rebuild the normals from the displaced surface. Skip it and dents and ripples are completely invisible, however hard you push the geometry.
- Displace along a welded normal, shade with a crease-aware one. Otherwise every hard edge cracks open.
- Find the cursor with a ray cast, in object space. A flat projection is only correct at dead centre.
- Ripples need a trail, not a single source. Otherwise all motion freezes the instant the mouse stops.
MIT © Meet Chauhan