Interactive Mesh
Interactive mesh gradient: three blurred color spots follow the cursor with CSS easing, over a static radial layer — pure DOM, no canvas.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Backgrounds has 50 effects, including 6 free. Explore the category →
3 usage examples



How it works
The effect uses three <div> elements (.interactive-mesh-blob) positioned absolute with border-radius: 50% and filter: blur(40px). This CSS filter turns each solid colored disc into a diffuse light spot; layered together, the three spots merge into a mesh gradient whose color zones overlap according to the pointer position.
On mousemove, the script computes the cursor position relative to the container, then repositions each blob via style.left and style.top — with a unique offset per blob (dx, dy as fractions of the container's width/height). Each blob orbits the cursor at a different distance, creating a constellation of colors in constant motion.
The smooth glide is entirely handled by the CSS rule transition: left .3s ease-out, top .3s ease-out — no requestAnimationFrame loop needed. The browser interpolates the positions: the ease-out curve gives the tracking an elastic character, responsive at the start of a gesture and smooth at the end.
A fourth static layer, .interactive-mesh-static, is a dual radial-gradient (amber bottom-left, green top-right) with filter: blur(25px), pinned at inset: 0. It anchors the palette and enriches the background even when the cursor is outside the container.
Accessibility
- prefers-reduced-motion: not implemented in the delivered code — blobs always move on
mousemove. For motion-sensitive users, add a guardif (matchMedia('(prefers-reduced-motion: reduce)').matches) return;before attaching the listener. - No
aria-hiddenorrole="img"is set on the container. Addaria-hidden="true"if the effect is purely decorative, orrole="img" aria-label="…"if it should be announced by screen readers. - Interaction is
mousemove-only — no keyboard or touch response is defined. On touch-only devices (mobile), blobs stay at their initial CSS positions (static gradient). - The
span.interactive-mesh-label('Bougez la souris') is DOM text read by assistive technology; its content will be announced if the container is in the reading flow. filter: blurrenders on a composited GPU layer in Chrome, Firefox, and Safari — no notable motor-accessibility impact, but worth watching on low-end devices under CPU load.
Browser compatibility
Requires CSS filter: blur() and mousemove events — supported in all modern browsers. Zero external dependencies, no canvas.
Without <code>filter: blur</code> support (very old browsers), blobs appear as solid color discs — the visual effect differs but no JS error occurs. The <code>interactive-mesh-static</code> layer still renders a coherent background.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="interactive-mesh" id="interactiveMesh">
<div class="interactive-mesh-blob"></div>
<div class="interactive-mesh-blob"></div>
<div class="interactive-mesh-blob"></div>
<div class="interactive-mesh-static"></div>
<span class="interactive-mesh-label">Bougez la souris</span>
<!-- Optional: overlay text/CTA -->
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| .interactive-mesh-blob:nth-child(1) background | rgba(255, 0, 128, .6) | Color and opacity of the first blob (magenta). Opacity (0.4–0.7) controls blending intensity with the other blobs. |
| .interactive-mesh-blob:nth-child(2) background | rgba(0, 200, 255, .5) | Color of the second blob (cyan). Playing warm vs cool between blobs creates a sense of depth. |
| .interactive-mesh-blob:nth-child(3) background | rgba(128, 0, 255, .55) | Color of the third blob (violet). To add a blob, duplicate an .interactive-mesh-blob and add its {dx, dy} offset to the JS array. |
| .interactive-mesh-blob filter | blur(40px) | Blur intensity per blob. 25 px = sharp saturated spots, 60 px = ultra-diffuse gradient. Higher values increase GPU cost slightly. |
| .interactive-mesh-blob transition | left .3s ease-out, top .3s ease-out | Easing duration and curve. 0.15s = immediate shadow-like tracking, 0.6s ease-in-out = lazy floating feel. |
| offsets (JS array, line 3) | [{dx:-0.3,dy:-0.2},{dx:0.1,dy:0.15},{dx:0.25,dy:-0.1}] | Per-blob offset from the cursor, as fractions of container width/height. Shrink absolute values for a tight mesh, expand for dramatic spread. |
| .interactive-mesh-static background | radial-gradient(at 10% 80%, rgba(255,200,0,.3) 0, transparent 50%), radial-gradient(at 90% 20%, rgba(0,255,128,.25) 0, transparent 50%) | Always-visible static layer. Adjust positions (e.g. at 10% 80%) and colors to anchor a background hue even when the cursor is absent. |
FAQ
<div> elements are repositioned with style.left and style.top on every mousemove, and the transition: left .3s ease-out rule delegates interpolation to the browser's CSS engine. The result is as smooth as a RAF animation, without any JS loop.getElementById('interactiveMesh') — a single element. For multiple instances, extract the logic into an initMesh(el) function that accepts the container as an argument, then call it once per target element.mousemove, which is not fired on pure touch interfaces. On mobile, blobs stay at their initial CSS positions (static gradient). Add a touchmove listener to support touch: read e.touches[0].clientX/Y instead of e.clientX/Y.