Skip to content

Magnetic Field

Show a dot grid that bends away from the pointer and springs back.

import { MagneticField } from '@youj/motion/backgrounds'

Experimental: what this label means

Installation & usage

npm i @youj/motion

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use it where the idea is a live surface that reacts to you: a product hero, a dashboard landing, a 404. It is the most tactile of the grid backgrounds, so keep it to one per page, and keep it quiet behind reading-length text.

The field fills its nearest positioned ancestor and paints behind your content, so make the container relative and isolate.

import { MagneticField } from '@youj/motion/backgrounds'

export function LiveHero() {
  return (
    <section className="relative isolate min-h-[560px]">
      <MagneticField />
      <p className="relative text-5xl font-semibold">Every signal, held in place</p>
    </section>
  )
}

spacing is the grid pitch in CSS pixels. A finer pitch looks denser and costs more dots: at the default 18px a 1440 x 900 screen draws about 4,000. Dots resting in place are one color; pushed dots take the second color, brighter and a little larger the further they are displaced.

Reference

Propscolors, intensity, speed… (8)

Props

MagneticField props
PropType
colors[dots: string, bent: string]

[dots, bent] CSS colors. Default: --color-fg-subtle dots that take --color-accent as they are pushed.

–
intensitynumber

Dot opacity, 0–1.5.

default 1

1
speednumber

Spring speed multiplier.

default 1

1
spacingnumber

Grid pitch in CSS px.

default 18

18
radiusnumber

Reach of the pointer in CSS px.

default 130

130
pointer'repel' | 'attract'

Whether the dots bend away from the pointer or toward it.

default 'repel'

'repel'
interactiveboolean

Pointer reactivity; a press shoves the dots near it.

default true

true
classNamestring–
PerformanceCanvas 2D, a handful of paths per frame

Performance

It's drawn with Canvas 2D, not WebGL. Every dot is steered by a spring each frame, which is a few multiplications per dot, and the dots are batched into six paths by how far they are displaced, so a frame is six fills. The canvas renders at a device-pixel ratio capped at 2. On the low motion-budget tier the pitch widens by 30%, which keeps about 60% of the dots, and the loop runs at 30 fps. The loop pauses when the field is offscreen or the tab is hidden, and it stops and cleans up on unmount. If the browser can't create a 2D context, it falls back to a static CSS wash of the accent color.

MotionSpring, pointer strength · reduced motion

Motion

Motion tokens
MomentTokens
Bendeach dot springs toward a target pushed away from the pointer
no token

damped spring, speed ÷ time scale

damped spring, speed ÷ time scale
Pressan outward shove near the press point
no token

impulse, then spring

impulse, then spring
Pointer strengtheases in and out with the pointer
no token

smoothed per frame

smoothed per frame

Reduced motion

Draws one still grid, bent around a fixed point: the same composition every time. There's no spring and no pointer response, and it redraws only when the size, the props or the theme change.

Reduced motion

The grid is bent around one fixed point, so it's the same on every visit and in every screenshot, and it still shows what the field does. Only the spring and the pointer response are gone. Switch on Reduced motion in any preview above to compare.

AccessibilityARIA

Accessibility

  • It's decorative: the element is aria-hidden and ignores pointer events.
  • Dots are small and low in contrast by default, but they cross whatever sits above them. Keep a quiet pool of canvas behind the copy, as the hero preview does, or lower intensity, and check your text's contrast there.
  • Nothing moves until the pointer does, and the field stops offscreen. Under reduced motion it doesn't move at all.