Magnetic
Pull an element toward a nearby pointer and return it when the pointer leaves.
import { Magnetic, MagneticInner } from '@youj/motion'
Installation & usage
npm i @youj/motionFor 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 on one or two calls to action that should feel alive: a hero's primary button, a floating action. It rewards the pointer on its way to the target. Keep it out of dense interfaces like toolbars, forms, menus and tables, where targets must stay exactly where they are, and don't put it on text.
Wrap the control. Magnetic is an inline-block wrapper that moves by transform, so the control keeps its layout, its semantics and its focus ring.
import { Magnetic, MagneticInner } from '@youj/motion'
import { Button } from '@youj/react'
export function Cta() {
return (
<Magnetic strength={0.3} radius={48}>
<Button size="lg">
<MagneticInner>Start deploying</MagneticInner>
</Button>
</Magnetic>
)
}Description
A hero with a primary and a secondary call to action that pull toward the pointer, the label inside lagging behind.
Buttons at different strengths, each inside a dashed outline that shows where the pull begins.
The pull starts radius pixels outside the element's box and grows linearly to full strength at the box's edge, so there's no jump when the pointer crosses into range. strength is the fraction of the pointer's offset from the center that the element follows: at 0.35, a pointer 20px right of center moves the element 7px. Keep it between about 0.15 and 0.5.
The dashed outlines show each zone. It's measured from the element's rectangle, so a round button still has a rounded-square zone.
The same button with no inner layer and with MagneticInner at different ratios.
MagneticInner goes inside the control, around its label or icon, and travels ratio as far as the shell. The default, 0.5, lets the label lag behind the button, which reads as depth. At 0 the label holds still while the button slides beneath it, and above 1 it runs ahead.
MagneticInner reads the nearest Magnetic around it. Outside one it renders a plain inline-block span.
Code
magnetic-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsstrength, radius, spring… (6)
Props
| Prop | Type | |
|---|---|---|
strength | numberFraction of the pointer offset the element follows. default 0.35 | 0.35 |
radius | numberPull starts this many px outside the element's box and grows toward it. default 80 | 80 |
spring | SpringToken'snappy''smooth''settle'default 'smooth' | 'smooth' |
children | ReactNode | – |
Other props pass through: extends HTMLMotionProps<'div'>. | ||
MagneticInner
| Prop | Type | |
|---|---|---|
ratio | numberHow far the inner content travels relative to the shell. 0.5 = half as far. default 0.5 | 0.5 |
children | ReactNode | – |
Other props pass through: extends HTMLMotionProps<'span'>. | ||
MotionPull, return, magneticInner · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Pullstrength × the pointer offset, fading to zero at radius | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Returnthe pointer leaves the zone or the page, or the window loses focus | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
MagneticInnerrides the same spring at ratio of the travel | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 |
Reduced motion
No pull. The element stays in place and works as usual.
spring swaps the default for another spring token. snappy tracks the pointer more tightly, and settle adds a small overshoot on the way home. Only transform moves, so the pull never triggers layout.
Reduced motion
Magnetic doesn't listen to the pointer at all, and the control sits exactly where the layout put it. Nothing else changes: the button is fully usable, and MagneticInner renders its content in place.
AccessibilityKeyboard, ARIA, focus
Accessibility
- Magnetic adds no roles and doesn't take focus. The control inside keeps its own name, keyboard behavior and focus ring.
- The pull moves the element toward the pointer, so the target gets closer, never harder to hit.
- It's off when the primary pointer is coarse, as on phones and tablets. Keyboard focus never pulls, so for keyboard users the control stays put.
- Keep the pull small. A button that travels far from its label, or away from the pointer, feels unstable, and moving targets are hard to use for people with motor impairments.