# Ferrofluid Magnet

> 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.

Canonical: https://gsapvault.com/effects/ferrofluid-magnet
Live demo: https://gsapvault.com/demos/ferrofluid-magnet/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | webgl-shader, metaballs, fluid-simulation, magnetic-attraction, pointer-tracking |
| Uses Lenis | No |

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### 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.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Ferrofluid Magnet](https://gsapvault.com/effects/ferrofluid-magnet)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Parallax Hero](https://gsapvault.com/effects/parallax-hero.md): A pinned hero that pulls its layers into depth from one scrubbed ScrollTrigger: a headline sandwiched inside the stack, ridges or photographs climbing over it, and a sun that sets behind the far layer.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
