All components

Magnetic Dots

Backgrounds

A dot grid backdrop driven by a real spring, not a lerp. Dots carry velocity, so they get flung past their rest position and ring back — and the field reads how fast you move, not just where you are: a swipe throws dots further than the same path taken slowly. Click for an expanding shockwave. The canvas is pointer-transparent and listeners bind to the container, so it keeps reacting under headlines and buttons. Fixed 1/120s sub-stepping keeps the spring stable and frame-rate independent; the dirty rect bounds the cost to the influence radius.

0 dots · bouncy

Move across the text.

The dots keep reacting under solid content. Swipe fast and they get thrown further than the same path taken slowly. Click for a shockwave.

Stiffness · 90
Damping · 9
Cursor drag · 0.9

Critical damping is 2·√stiffness — about 19 here. Below it the dots overshoot and ring; above it they crawl home. Drop drag to 0 and the field stops caring how fast you move, which is how a lerped grid always behaves.

Installation

Terminal
npx shadcn@latest add https://ui.saumyarex.xyz/r/magnetic-dots.json

Props

PropTypeDefaultDescription
stiffnessnumber90Spring constant pulling each dot home. With damping, this is the entire feel of the component — higher is snappier with a faster wobble.
dampingnumber9Velocity damping. Critical is 2·√stiffness; the default sits below it so dots overshoot and settle back visibly. Above it the motion turns to syrup.
dragnumber0.9How much pointer velocity drags dots along with it. Set to 0 and the field reacts only to cursor position, the way a lerped grid does.
shockwaveboolean | ShockwaveOptionstrueExpanding ring of force on pointer-down. An analytic annulus rather than a coupled lattice — same look, and it keeps the per-frame cost bounded.
pointerTarget"container" | "window""container"Where pointer listeners bind. Container reacts within this element's box; window keeps a full-page backdrop live wherever the cursor is.
maxDisplacementnumber46Hard cap on how far a dot can leave its grid position. Bounds the dirty rect as well as the chaos; the outward velocity component is stripped at the cap so pinned dots don't snap back.
strengthnumber2200Peak push force at the cursor, in px/s². A force, not a distance — how far a dot actually travels also depends on stiffness and damping.
radiusnumber170Influence radius in CSS pixels. Also sizes the dirty rect, so it — not the grid — determines per-frame cost.
spacingnumber22Grid pitch in CSS pixels; this drives the dot count. Changing it rebuilds the grid, while everything else retunes the running loop in place.
attractbooleanfalsePull dots toward the cursor instead of pushing them away.
colorstringcurrentColorAny canvas-valid color. Defaults to the inherited currentColor, re-resolved when the theme flips since a canvas can't inherit it live.
onMeasure(info) => voidFires on layout changes only, never per frame — safe to setState in. Reports dot count, cols, rows, and DPR.

Dependencies

clsxtailwind-merge