089

Ferrofluid Magnet

£5

Beads of glossy black ferrofluid scattered across a hero: your cursor is an invisible magnet that tugs gently on hover; press and hold to pull the fluid into a spiked pool (or shove it clear in repel mode), then let go to throw it outward.

advanced
5 more details
webgl-shadermetaballsfluid-simulationmagnetic-attractionpointer-tracking
Ferrofluid Magnet - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

A hero background of glossy black ferrofluid, rendered in WebGL. Beads of fluid lie scattered across the section, and your cursor is an invisible magnet with two strengths. Hovering only tugs the nearest beads towards it; pressing and holding switches it fully on, so the fluid streams in, merges into one pool and bristles into a crown of spikes that catch the light. Let go and the pool is thrown outward.

Read the full effect overview

Flip it to repel and the same hold shoves the fluid clear instead, opening a hole with the spikes standing round its rim. Nobody steering? A faint magnet idles in a slow loop so the fluid is never quite still, and anything you mark as keep-clear, such as your headline, stays free of fluid so the copy on top is always readable.

It is three.js and a small bead simulation, not a video or a sprite sheet. It works with mouse, touch and keyboard, matches its floor to your page colour, and falls back to a still CSS pool of ferrofluid without JavaScript, without WebGL, or under reduced motion.

What's included

10 items
  • Two magnet strengths: a gentle tug on hover and a full pull while the pointer is held, easing smoothly between them
  • Attract and repel modes, switchable live from your own buttons with one custom event
  • Held fluid rises into a crown of needle spikes with uneven heights and pin-point highlights, centred on the magnet or ringed round the hole it opens
  • Letting go throws the fluid outward, harder the longer you held
  • Beads keep their volume and cling together as they meet, so the fluid pools and splits like a liquid rather than a cloud of dots
  • Keep-clear zones: mark any element and the fluid is nudged out from behind it, so headlines stay readable
  • An idling magnet drifts round a resting place you choose, with a separate resting place for narrow screens
  • Keyboard route: hold Space to press, arrow keys to move the magnet's resting place
  • Frame-rate independent, paused while off screen, and disposes every GPU resource and the WebGL context on teardown
  • Composed still-pool fallback for no JavaScript, no WebGL and reduced motion

Perfect for

5 use cases
  • Agency and studio landing pages that want one tactile moment above the fold
  • Product launches for hardware, audio or materials brands where a glossy, physical look fits the product
  • Portfolio and personal sites where the hero should invite play before the scroll
  • Campaign and event microsites built around a single striking interaction
  • Science, engineering and design-school pages that want to show magnetism rather than describe it

How it works

5 sections

Beads, not a mesh

The fluid is a pool of individual beads, mostly small with a few larger drops. Each frame they are pushed by the magnet, carried along in its wake when it moves, stirred by a slow current, and kept apart or pulled together by their neighbours. That last part is what makes them behave like a liquid: touching beads hold their volume and cling, so a pool forms, stretches and tears apart instead of overlapping into a blur.

From beads to a glossy surface

The beads are never drawn as circles. They are blended into one smooth field, and a single shader decides where that field crosses into fluid and how the surface curves. Where beads merge the surface bulges and joins seamlessly, which is the metaball look. It is lit by a soft studio reflection with a sharp specular highlight and a contact shadow on the floor, so it reads as wet black lacquer rather than flat ink.

The magnet and the spikes

The magnet follows your pointer with a little weight, or idles round its resting place when nobody is steering. Its strength is played rather than simulated: it fades in on load, eases up when you press and back down when you let go, and a short outward throw fires on release. Spikes only rise once enough fluid has gathered under a held, fairly still magnet, and they flatten again while it is dragged fast or released.

Staying out of the way

Elements you mark as keep-clear become zones the beads are nudged out of by the shortest route, and beads never start inside them. The simulation is scaled down for narrow containers, rescales in place when the container resizes, and stops entirely while the section is off screen.

Fallbacks and teardown

Support is checked before first paint, so a visitor who will not get a canvas sees the still pool from the start rather than a flash of it. Reduced motion is watched live, and tearing the effect down stops the loop, removes every listener and releases all of its GPU memory and the WebGL context.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Paid effect

Purchase to unlock the code.

Buying Ferrofluid Magnet opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The inline script is not optional. The still pool (.ferro__still, a glossy drop drawn in CSS) is the fallback for no JavaScript, no WebGL and reduced motion, and without this it paints for the half second it takes three.js to arrive, then gets swapped for the canvas. The probe runs before first paint, stamps html.gl when a canvas is coming, and the stylesheet hides the still under that class. No JavaScript, no class, the still shows; and the effect removes the class again if it cannot build a renderer after all.

2. Add to your <body>:

Code snippet omitted: it ships with the download.

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

The script finds every [data-ferro] element, inserts a canvas as its first child, and hides the still once the canvas is actually there.

Why the import map rather than a plain <script src> for three.js: three ships as ES modules only, and its old UMD build logs a deprecation warning on every page load. The shim above imports it as a module, puts it on window, and then loads assets/script.js as an ordinary script, so the effect itself stays a plain file you can drop into any build (or none).

Already using three.js as a module? Then skip the shim, and just make sure window.THREE is set before assets/script.js runs:

Code snippet omitted: it ships with the download.


Using It With Your Own Design

What the effect actually requires of your markup: one element with data-ferro. Everything inside it is yours: the canvas is inserted behind your content, and any child marked data-ferro-avoid (or matching your own data-avoid selector) becomes a zone the fluid is nudged out of, so a headline on top stays readable. Add tabindex="0" and an aria-label if you want the keyboard route (see Accessibility).

The ground colour comes from your CSS. The canvas paints its own floor, and it reads that colour from the --page custom property on the [data-ferro] element when the effect starts. Set --page to your section's background and the canvas and the page match exactly; any CSS colour syntax works. The fluid itself is always glossy black, so the effect wants a light to mid ground.

Non-obvious things the effect depends on (keep these if you restyle):

  • The container must have a real size. The canvas is sized from the container, so .ferro needs a height from somewhere: it is min-height: 100svh in the demo. Give it a height of 0 and you get a canvas of 0. It also needs position: relative (the canvas is absolutely positioned inside it) and overflow: hidden.
  • isolation: isolate on .ferro plus z-index: -1 on .ferro__canvas. That pair is what puts the canvas behind your content without it escaping behind the section's own background.
  • The .ferro__still element must stay in the document. It is the fallback for no JavaScript, no WebGL and reduced motion, and it is only hidden by html.gl (the head probe, before first paint) and .ferro.is-live (the script, once a canvas exists).
  • touch-action: pan-y on .ferro. It keeps vertical swipes scrolling the page on touch; a horizontal drag or a press-and-hold drives the magnet.
  • user-select: none on .ferro, so a press and drag moves the fluid instead of selecting your headline.
  • Keep-clear zones are measured when the effect starts and again whenever the container resizes. If you move or reveal avoided content later without the container changing size, the fluid will not know.

What is only the demo's CSS and can be deleted: .demo-title, .demo-controls and .demo-controls__btn, the Mona Sans import, and the token values in :root other than --page. The Attract / Repel buttons and the "Hold to attract" label are wired up in the init as a demo of the public mode event (see Programmatic Control), not by the effect itself.


Options

All set on the [data-ferro] element.

Attribute Values Default Description
data-mode attract, repel attract Attract streams the fluid in and pools it under the magnet, spikes at the centre. Repel shoves it aside and raises the spikes round the rim of the hole. Anything other than repel means attract.
data-magnets x,y fractions 0.5,0.5 The magnet's resting place as a fraction of the container's width and height. It idles round this point whenever no pointer is over the effect.
data-magnets-narrow x,y fractions same as data-magnets Resting place used instead when the container is narrower than 640px at start-up, so you can move the magnet clear of stacked mobile copy.
data-beads 10 to 300 150 fine pointer, 110 touch, 80 narrow How many beads of fluid. More beads make a bigger, heavier pool and cost more CPU.
data-reach px 260 How far the magnet pulls or pushes. A held magnet reaches further than a hovering one (60% further on wide containers, 15% on narrow ones).
data-hover 0 to 1 0.16 Magnet strength while the pointer merely hovers, as a share of the full held strength. 0 makes the effect press-only.
data-idle 0 to 1 0.1 Strength of the idling magnet when nobody is steering it. 0 lets the fluid settle into a slow drift of its own.
data-drift 0, 1 1 Whether the idle magnet wanders in its slow loop round the resting place. 0 holds it still on the spot.
data-spike-gap px 24 Spacing between spikes in the crown. Smaller is a finer, denser bristle.
data-spike-height number 60 How tall the spikes stand, which is how sharply they catch the light.
data-avoid CSS selector [data-ferro-avoid] Elements inside the container the fluid keeps clear of.

On containers narrower than 640px at start-up, data-reach and data-spike-gap are scaled to 80%, along with the beads themselves, so the fluid keeps its proportions on a phone.

Only the first x,y pair in data-magnets is used: there is one magnet.

Example: a quieter hero that only reacts to a press

Code snippet omitted: it ships with the download.

Example: repel, with a finer, taller crown

Code snippet omitted: it ships with the download.


Accessibility

  • Keyboard: give the container tabindex="0". With it focused, holding Space switches the magnet fully on and releasing throws the fluid, just like a press; the arrow keys move the magnet's resting place (hold Shift for bigger steps). Keys are only taken while the container itself has focus, so buttons and links inside it keep their normal behaviour.
  • Controls inside the effect still work: a press that starts on a link, button, input, select, textarea or label is left alone and never becomes a magnet press.
  • Touch: hovering does not exist on touch, so the magnet only acts while a finger is down. Press and drag to steer it; a vertical swipe that the browser takes over for scrolling lets go of the fluid quietly, without the throw.
  • Screen readers: the canvas and the still are both aria-hidden. The effect is decorative; describe the interaction in the container's aria-label as the demo does, and keep your real content in the markup on top.
  • Readable copy: marking your headline with data-ferro-avoid keeps the fluid out from behind it, so the text never sits on black.
  • Reduced motion: the WebGL scene never starts. The still pool stays exactly as it is, a single glossy drop resting on the page. The preference is watched live: switch it on while the page is open and the canvas is torn down and the still returns.
  • Without JavaScript, or without WebGL: the same still pool, for the same reason. One fallback, three failure modes, and it is never hidden until a canvas has actually been created.

Dependencies

Dependency Version Required
three.js 0.180.0 Yes, as an ES module (see Quick Start)
GSAP core 3.15.0 (works on 3.12+) Yes: matchMedia branching, the ticker the frame loop runs on, the magnet's fade-in, press and release tweens, and teardown

No GSAP plugins, and no Lenis. GSAP does not move the fluid: every bead is simulated each frame in plain JavaScript, and the shaders draw the result. What GSAP owns is the clock that loop runs on and the few values that are played rather than simulated: the magnet fading in on load, easing up to full strength when you press, easing back when you let go, and the outward throw on release. Because both run on the same ticker, the tweens and the simulation that reads them never disagree by a frame.

Everything the effect uses (WebGLRenderer, WebGLRenderTarget, OrthographicCamera, Points, BufferGeometry, PlaneGeometry, ShaderMaterial) is long-stable three.js API, so pinning to a different version is a one-line change in the import map.


Browser Support

Anything with WebGL2, which is every current browser. The fluid is built from a floating-point field texture, which WebGL1 cannot reliably render into, so on a WebGL1-only device the effect declines to start and the still pool is what shows.

Support is probed before three.js is asked for a renderer, deliberately: three logs its own failure to the console, as errors, several times over, before it throws, so catching the exception would hide nothing and a visitor with WebGL disabled would get a console full of red on a page that had quietly fallen back. Probed first, that visitor simply gets the still pool and a clean console.

Import maps are supported everywhere current. In a browser old enough to lack them the module never runs, window.THREE is never set, and the still pool is again what shows. The same happens if GSAP fails to load.

If the browser drops the WebGL context (a GPU reset, too many tabs), the loop pauses and picks up again when the context is restored.

Performance

Two draw calls a frame: every bead is drawn into an offscreen field in one pass, then one full-screen pass lights the fluid from it. The GPU side is cheap. The expensive part is the bead simulation on the CPU, where each bead checks its near neighbours so the fluid keeps its volume and clings together; that cost grows quickly with the bead count. The loop does not run at all while the effect is off screen, and pixel ratio is capped at 2 (1.5 on touch devices). Resizing the container rescales the fluid in place rather than restarting it.

To buy back frames on low-end hardware: lower data-beads first. It is the simulation cost, and a smaller pool of larger-looking fluid still reads well; the narrow-screen default of 80 is a good floor to start from.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

The Vault library, plus future additions to the library.