Atmosphere✨ Premium

Attraction Field

80 particles attracted toward the cursor with F = min(2, 50/d) — links between neighbours within 80 px, 0.96 damping per frame, toroidal wrapping.

JSCanvasInteractive

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. Atmosphere has 57 effects, including 10 free. Explore the category →

3 usage examples

Hero / Landing — Interactive background for a data or network SaaS — Attraction Field example 1

① Hero / Landing — Interactive background for a data or network SaaS

WhenThe landing page presents a data analytics, connection-mapping, or networking tool — the effect reinforces the visual theme without external assets.
WhyParticles converging toward the cursor create a data-centralisation metaphor and invite interaction, increasing dwell time on the page.
Settings80 particles, colour rgba(139,92,246,...). Add a scrim rgba(10,10,15,0.55) between the canvas and the text to ensure a contrast ratio ≥ 4.5:1 on the headline.
Brainstorming canvas — Ideas gravitate toward your focus — Attraction Field example 2

② Brainstorming canvas — Ideas gravitate toward your focus

WhenA collaborative thinking app (mind map, FigJam-like workspace) opens a blank canvas — particles embody ideas that converge toward the user's point of concentration.
WhyThe physics metaphor is direct: each particle is a thought, links are conceptual connections. Attraction toward the cursor translates the very act of 'focusing' on an idea, making the visual space alive and expressive.
Settings80 particles, colour rgba(139,92,246,...), link threshold 80 px, damping 0.96. For an airier feel reminiscent of an open creative workspace, extend the threshold to 100 px and lower link opacity to 0.10 for even subtler connections.
Conversion micro-interaction — Pricing page or upgrade modal background — Attraction Field example 3

③ Conversion micro-interaction — Pricing page or upgrade modal background

WhenThe pricing page or an upgrade modal displays the Pro plan — the effect runs in the background to guide attention toward the CTA.
WhyParticles gravitating toward the cursor create a visual subtext of 'everything converges here' at the precise moment the user is deciding.
SettingsReduce particle count to 40 (e < 40) and increase force to 80 (80/d) for more dramatic, readable motion in a smaller area.

How it works

An IIFE initialises a full-width canvas via setupCanvas('psAttraction') (getElementById lookup). A pool of 80 particles is created at startup with random positions, zero velocity, and radii between 1 and 3 px. The cursor is tracked via a mousemove listener on the canvas, with CSS-to-canvas coordinate conversion through getBoundingClientRect(). A non-cancellable requestAnimationFrame runs continuously — there is no IntersectionObserver and no off-screen pause.

On each frame, each particle computes the vector to the cursor (dx, dy) and the distance d. The attraction force is F = min(2, 50/d) — increasing at close range, capped at 2 to prevent numerical instability. The applied acceleration is F × 0.02, a maximum of 0.04 px/frame². Velocity is damped each frame by a factor of 0.96 (natural stop in ~25 frames if force ceases). Position is updated, then toroidally wrapped: a particle leaving one edge reappears at the opposite edge.

Rendering starts with a full #0a0a0f fill (fillRect of the entire canvas, not a clearRect). Each particle is drawn as a circle rgba(139, 92, 246, 0.4 + r/5) — small particles (r ≈ 1) have 0.6 opacity; large ones (r ≈ 3) reach 1.0. A double loop then checks all pairs: if the distance between two particles is below 80 px, a 0.5 px line is drawn with opacity 0.15 × (1 − d/80) — connections fade out gradually with distance.

Accessibility

  • prefers-reduced-motion absent: the code does not check this media preference — the animation runs in a perpetual loop regardless of the system setting. Add a matchMedia('(prefers-reduced-motion: reduce)') guard manually for accessible builds.
  • The <canvas> lacks aria-hidden="true" in the supplied structure — add it so screen readers skip the canvas element.
  • No role="img" or aria-label on the .demo-preview container — the effect has no semantic description.
  • Attraction is triggered solely by mousemove — no touch (touchmove) or keyboard equivalent. On mobile, particles drift toward the default centre position.
  • Particle contrast: #8b5cf6 purple on #0a0a0f reaches ~4.7:1 at full opacity (WCAG AA for graphical elements); links at alpha 0.15 fall below the threshold — they are decorative, not informational.

Browser compatibility

Requires Canvas 2D Context and requestAnimationFrame — supported in all modern browsers. Zero external dependencies.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Fixed centre (no mousemove)
Android Chrome✓ Fixed centre (no mousemove)

If <code>canvas</code> is not supported, the container stays empty (no fatal JS error). If <code>getElementById('psAttraction')</code> finds no canvas, the IIFE returns immediately without starting a loop.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="demo-preview">
  <div class="ps-canvas-wrap" role="img" aria-label="Purple particles attracted toward the cursor with links between close neighbours">
    <canvas class="ps-canvas" id="psAttraction" aria-hidden="true"></canvas>
    <span class="ps-hint" aria-hidden="true">Move the mouse</span>
    <!-- Optional: content overlay (scrim + title + CTA) -->
    <!-- <div class="ps-overlay"> ... </div> -->
  </div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Particle count [JS: < 80] 80 Spawn loop: for (let e = 0; e < 80; e++). 40–60 is more appropriate for an embedded component; exceeding 120 strains mobile browsers.
Particle radius [JS: 2 × rand + 1] 1–3 px Controls size and opacity (opacity = 0.4 + r/5). Replace with rand + 0.5 for finer particles; 3 × rand + 2 for large, bright ones.
Attraction constant [JS: 50] 50 Numerator in min(2, 50/d). Increase to extend the attraction range; decrease to concentrate it near the cursor.
Acceleration factor [JS: 0.02] 0.02 Multiplied by force each frame: dx/d × force × 0.02. 0.02 gives fluid attraction; 0.05 makes it snappy and unstable.
Damping [JS: 0.96] 0.96 Per-frame deceleration: vx *= .96. 0.96 = natural stop in ~25 frames. 0.99 = long coasting trail, 0.90 = abrupt stop.
Link threshold [JS: 80] 80 px Maximum distance between two particles to draw a link: if (i < 80). Increase for a denser graph; reducing to 50 thins it out.
Particle colour [JS: 139,92,246] purple #8b5cf6 RGB values in rgba(139,92,246,...) (particles and links). Replace the three integers with your brand colour's RGB components.

FAQ

No: the code contains no IntersectionObserver. The requestAnimationFrame loop runs continuously even when the canvas is outside the viewport. To optimise performance, wrap the loop in an IntersectionObserver that stops the RAF when the intersection ratio drops to zero and restarts it when the canvas re-enters.
In its current form, the effect only listens to mousemove. On mobile, particles drift toward the default centre position. To support touch, add a touchmove listener on the canvas that updates the tracking variables with evt.touches[0].clientX and clientY, converted via getBoundingClientRect().
The effect uses a fixed id 'psAttraction'. In React, mount the canvas via a ref inside useEffect, extract the particle logic into an initAttraction(canvas) function that takes the canvas element directly (not an id), and cancel the requestAnimationFrame in the cleanup return. In Vue, the same pattern applies via onMounted / onUnmounted.