Liquid✨ Premium

WebGL Fluid Simulation

A real fluid simulation on the graphics card: neon ink follows the cursor, swirls, fades, a click bursts a color and an invisible hand keeps stirring when nobody touches — WebGL2 with a WebGL1 fallback, sharp on Retina screens, paused off screen, a still image when the user reduces motion.

WebGLShaderFluidInteractiveBloom

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →

3 usage examples

Bath-marbled paper — website of a fine bookbinder — WebGL Fluid Simulation example 1

① Bath-marbled paper — website of a fine bookbinder

WhenThe heading "The marbling workshop" at the top left, over a background of swirling inks; the mouse draws swirls like the marbler's comb, a click drops a new color.
WhyThe ink is truly dragged by the gesture and forms swirls that linger for a while, as on the marbling bath; an animated gradient would move on its own without answering the hand.
Settingsdata-wfl-palette="aurora", data-wfl-keep="0.8" (longer-lasting ink), data-wfl-swirl="14" (softer swirls), data-wfl-bloom="0" (no glow, an inky look); full-frame scene, the template's .wfl-scrim overlay behind the 30 px Georgia heading.
Touch wall in the sensory room — special needs education center — WebGL Fluid Simulation example 2

② Touch wall in the sensory room — special needs education center

WhenOn the large wall screen at child height, each gesture leaves a glowing trail and each tap a burst of color; between two children, the screen keeps moving gently.
WhyAn immediate, gentle response, with no sound or text; the invisible hand keeps the screen alive between children, and the device's reduced motion setting gives a still image for a child who gets agitated by movement.
SettingsDefault settings: data-wfl-palette="neon", data-wfl-bloom="1", data-wfl-idle="1", .wfl-paint class for finger painting; screen framed by a 6 px border and 18 px corners, "Rainbow room" plate below.
VJ-driven visuals — an electronic music collective's night in a disused warehouse — WebGL Fluid Simulation example 3

③ VJ-driven visuals — an electronic music collective's night in a disused warehouse

WhenFull-screen projection behind the DJ: the VJ paints with the mouse to the beat, and clicks on the strong beats burst the colors; the booth stands out in silhouette in front.
WhyComputing on the graphics card keeps up at large sizes (120 frames per second measured up to 1,789 × 1,006); a 2D canvas computed pixel by pixel could not follow.
Settingsdata-wfl-swirl="34" (stronger swirls), data-wfl-keep="0.42" (shorter-lived ink, the frame keeps renewing), data-wfl-bloom="1"; booth and crowd as black SVG laid on top with pointer-events: none.

How it works

The .fx-0841-root block holds a 16/9 .wfl-scene (14 px corners, #0a0a0f background, crosshair cursor) with a <canvas class="wfl-canvas"> filling it, and a demo label that fades on hover. The script creates one fluid per .wfl-scene on the page and sets it up through attributes: data-wfl-palette (neon, aurora or mono with data-wfl-hue), data-wfl-keep (share of ink kept each second, 0.55), data-wfl-swirl (swirl strength, 22), data-wfl-bloom (light glow, 1), data-wfl-idle (invisible hand, 1) and data-wfl-ink (amount of ink, 1). Each scene also exposes scene.wfl.set(), burst(x, y), clear() and state().

The simulation follows Jos Stam's "stable fluids" method (1999), entirely in shaders. On every frame: the pointer injects velocity and ink along its path; vorticity confinement revives small swirls; pressure is solved with 18 Jacobi iterations (12 on a frame under 700 px) so the fluid does not compress; then velocity and ink are carried along by the flow. Velocity is computed on a grid of 128 cells on the short side, ink on 512 (384 under 700 px), the display at screen density (up to 2, within a limit of 1.8 million pixels). Dull mixes fade faster than vivid colors, and each new color stays close to the previous one on the hue wheel: the ink never turns brown or gray. The display adds a light relief lit from the top left, a 4-level glow on the brightest areas and a half-shade grain that prevents banding in gradients. Time steps follow the real clock: same speed at 60 and 120 Hz.

A click bursts a blot of 6 to 8 spinning jets. After 2.5 s without a pointer, the invisible hand takes over: a stroke drifting slowly across the frame and a burst every 2.5 to 5 s, so the background stays alive. Computing stops off screen (IntersectionObserver) and when the tab is hidden. A block hidden at load (tab, closed accordion) creates its buffers as soon as it gets a size. If the browser takes back the WebGL context, the script waits for it to be restored and starts again. Without WebGL, the scene gets .wfl-nogl: four still neon blots in CSS gradients, never an empty frame. Measured with the graphics card in Chromium and Firefox: 120 frames per second up to a 1,789 × 1,006 canvas, under 0.1 ms of JavaScript per frame; with software rendering (no acceleration), 49 frames per second at 640 × 360 and 25 at 1,789 × 1,006.

Accessibility

  • Reduced motion: under prefers-reduced-motion: reduce, the script paints a still image (five ink blots swirled in one go) and starts neither the loop nor the invisible hand; measured in Chromium, Firefox and WebKit, 0% of pixels change at rest. A visitor's gesture makes the fluid move for 1.5 s, then everything stops: motion only comes from them. A block hidden at load keeps this behavior when it shows up.
  • Screen readers: the canvas carries role="img" and an aria-label describing what is shown; the demo label is aria-hidden="true". As a decorative background under a heading, replace the label with a short description or set the canvas to aria-hidden="true".
  • Touch and keyboard: on a .wfl-paint scene, the finger paints and the page does not scroll inside the frame (touch-action: none); without that class, the finger scrolls the page and only paints while touching. With a keyboard nothing happens: the effect is pure decoration, no information should depend on it.
  • Text on top: the CSS provides a banner template (.wfl-scrim, .wfl-hero-content, .wfl-title, .wfl-btn) with a gradient overlay behind the text; under prefers-contrast: more or prefers-reduced-transparency: reduce, the text moves onto a solid panel and the fluid is dimmed.

Browser compatibility

WebGL2 (RGBA16F textures) with a WebGL1 fallback (half floats, then floats, then encoded 8 bits), linear filtering redone by hand if the graphics card cannot filter floats: 38 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: WebGL2 active, canvas at 1,280 × 720 for a 640 × 360 scene on a Retina screen, paused off screen, still image under reduced motion, a scene hidden at load starting when shown, 0 errors. The WebGL1 fallback, forced in all three engines, runs the same. Without WebGL (tested in Chromium), the scene shows its still gradient.

Chrome 56+✓ Full (WebGL2)
Firefox 51+✓ Full (WebGL2)
Safari 15+✓ Full (WebGL2); earlier, WebGL1 fallback
Edge 79+✓ Full (WebGL2)
Mobile iOS 15+✓ Full, by touch
Android Chrome 56+✓ Full, by touch (smaller grids under 700 px)

Without WebGL (old browser, acceleration turned off), the scene gets .wfl-nogl: four still neon blots in CSS gradients. Without JavaScript, the frame stays black with its label. Without ResizeObserver, the script follows the window resize event.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0841-root">
  <div class="wfl-scene wfl-paint" data-wfl-palette="neon" data-wfl-keep="0.55">
    <canvas class="wfl-canvas" role="img" aria-label="…"></canvas>
    <div class="wfl-hint" aria-hidden="true">
      <span>WebGL Fluid Simulation</span>
      <span class="wfl-hint-sub">…</span>
    </div>
  </div>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Palette (HTML — data-wfl-palette, data-wfl-hue) neon neon runs from lime green to hot pink without ever going through red or yellow; aurora goes back and forth through green, cyan, blue, violet; mono plays on a single hue, chosen with data-wfl-hue from 0 to 1 (0.55: cyan blue, 0.8: violet).
Ink persistence (HTML — data-wfl-keep) 0.55: the ink keeps 55% of its strength each second 0.8 leaves swirls that stay for several seconds; 0.3 fades quickly, for a background that keeps renewing under text. From 0.05 to 0.98.
Swirls (HTML — data-wfl-swirl) 22 10 to 15 for slow, broad swirls; 30 to 40 for a lively fluid full of small eddies.
Light glow (HTML — data-wfl-bloom) 1 0 removes the glow: a matte ink look, and a bit less work for the graphics card.
Invisible hand (HTML — data-wfl-idle) 1: takes over after 2.5 s without a pointer 0 lets the fluid settle and fade out when nobody touches. The delay is set in the JS (IDLE_AFTER = 2500, in ms).
Amount of ink (HTML — data-wfl-ink) 1 0.6 for a discreet background behind text; 1.5 for denser, brighter strokes.
Script control (JS — scene.wfl) set, burst, clear, state scene.wfl.set({ palette: 'aurora', keep: 0.7 }) changes settings on the fly; scene.wfl.burst(0.5, 0.5) bursts a color at the center (coordinates from 0 to 1, origin at the bottom left); scene.wfl.clear() empties the frame.

FAQ

Each .wfl-scene opens its own WebGL context, and browsers cap how many (around 16 in Chrome); only visible fluids compute. Three scenes on the same page run without trouble (measured in the examples). If the browser takes a context back for lack of memory, the script waits for it to be returned and rebuilds the fluid, including in a block that is hidden at that moment.
Fluid Gradient is a CSS background: four blurred blots turning on their own over 15 s, with no script and no reaction to the pointer. WebGL Fluid Simulation computes a real fluid: the ink is pushed by the gesture, swirls, mixes and fades. Pick Fluid Gradient for a discreet, light background; the simulation when the visitor's gesture is part of the experience.
The script lightens the load by itself: display density capped at 1.8 million pixels, smaller ink and pressure grids under 700 px wide. Measured with software rendering, the worst case: 49 frames per second at 640 × 360 and 25 at very large sizes. For a full-screen background on modest machines, set data-wfl-bloom="0". With no WebGL at all, the scene shows its still neon gradient.