Particle Field
An interactive particle field — particles drift freely and link up when they get close, with a cursor that repels or attracts them, drawn on canvas 2D.
Loading preview…
npx shadcn@latest add https://webberui.com/r/particle-field.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
60
2
0.5
130
<ParticleField />
Installation
npx shadcn@latest add https://webberui.com/r/particle-field.jsonOr, once registries are configured in components.json, install it as @webberui/particle-field.
Usage
The particle field is a decorative background: drop it into a relative container and decide the height yourself, then layer foreground content on top:
import { ParticleField } from "@/components/ui/particle-field";
<div className="relative h-[320px] overflow-hidden rounded-xl">
<ParticleField />
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<h2 className="text-2xl font-semibold">Hero heading</h2>
</div>
</div>Adjusting density, color, and interaction mode:
<ParticleField
quantity={90}
color="#22d3ee"
mode="attract"
linkDistance={150}
/>Plain drift with no links:
<ParticleField links={false} interactive={false} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
quantity | number | 60 | Number of particles; more means denser links, and the performance cost grows as O(n²) |
color | string | Theme-aware neutral | Particle and link color; when unspecified, neutral-400 in light mode and neutral-600 in dark |
particleSize | number | 2 | Radius of each particle at rest (px) |
speed | number | 0.5 | Drift speed multiplier; per-frame displacement lands roughly within ±speed |
links | boolean | true | Whether to draw links between nearby particles |
linkDistance | number | 130 | Maximum link distance (px) — the closer the particles, the more solid the line |
influence | number | 120 | Cursor influence radius (px); only particles inside it are affected |
mode | "repel" | "attract" | "repel" | Cursor interaction mode: repel or attract particles |
interactive | boolean | true | Whether cursor interaction is enabled (mouse pointers only; touch has no effect) |
className | string | — | Appended to the outermost container's className |
How it works
- A single canvas 2D layer. Every particle and link is drawn on the same
<canvas>, cleared withclearRectand redrawn each frame, so there is no explosion of DOM nodes and the frame rate stays stable even with dense links - Particles drift freely from a random initial velocity and bounce off the container edges, keeping the overall density even; initial coordinates are generated only inside
useEffect(client-side only), so there is no SSR hydration mismatch - Cursor interaction works as a "draw-time displacement" rather than a persistent velocity: particles within
influenceget a displacement computed from their distance —repelpushes them away,attractpulls them in (clamping the displacement so they never cross the cursor). Once the cursor leaves,strengthdecays smoothly back to 0 and the particles ease back onto their drift path - Links are drawn before the particles and fade out with distance (
globalAlpha) — the closer they are, the more solid the line. Links are computed from the "position drawn this frame", so the mesh warps along with the repulsion or attraction - The real canvas size is set from
devicePixelRatio(capped at 2) and then scaled withsetTransform, keeping it sharp on Retina displays; aResizeObserverwatches the container and clamps out-of-bounds particles back into range when the size changes - When
coloris unspecified, it reads the canvas'scurrentColor(resolved from thetext-neutral-*class) and uses aMutationObserveron the<html>element's class to re-read the color the moment the light/dark theme is switched
Accessibility
- The container carries
aria-hidden— as a purely decorative background it does not interfere with assistive technology - When the user has "reduce motion" enabled at the system level, the rAF loop and cursor interaction are both disabled and only a single static particle field is rendered, with no change to the DOM structure
- Keep enough contrast in your own foreground content; the particle layer defaults to a low-contrast neutral color and will not hurt the legibility of text above it