Cursor effect
Magnetic button: a button that leans toward the cursor
As the pointer comes near, the button drifts toward it and its label drifts a little further. When the pointer leaves, it settles back into place.
Usage
When to use it
- One primary call to action per page, such as a sign-up or a download button.
- Portfolio contact buttons and navigation dots in creative layouts.
- Large, round icon buttons where a bit of pull makes them feel alive.
- Use it sparingly. A page full of magnetic buttons feels slippery.
Code
Copy the effect
<div class="magnetic" data-strength="0.3">
<button class="magnetic-button" type="button">
<span class="magnetic-label">Start a cursor</span>
</button>
</div> .magnetic {
display: inline-block;
padding: 24px; /* the magnetic field around the button */
margin: -24px;
}
.magnetic-button {
display: inline-block;
padding: 14px 28px;
border: 1px solid #15161a;
background: #2d5bff;
color: #fff;
font: inherit;
cursor: pointer;
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.magnetic-label {
display: inline-block;
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.magnetic-button:focus-visible {
outline: 2px solid #15161a;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.magnetic-button,
.magnetic-label {
transform: none !important;
transition: none;
}
} (() => {
const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)');
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('.magnetic').forEach((zone) => {
const button = zone.querySelector('.magnetic-button');
const label = zone.querySelector('.magnetic-label');
const strength = parseFloat(zone.dataset.strength) || 0.3;
let frame = 0;
if (!button) return;
function reset() {
cancelAnimationFrame(frame);
frame = 0;
button.style.transform = '';
if (label) label.style.transform = '';
}
zone.addEventListener('pointermove', (event) => {
if (event.pointerType !== 'mouse' || !finePointer.matches || reduceMotion.matches) return;
const { clientX, clientY } = event;
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
const rect = zone.getBoundingClientRect();
const dx = clientX - (rect.left + rect.width / 2);
const dy = clientY - (rect.top + rect.height / 2);
button.style.transform = `translate(${dx * strength}px, ${dy * strength}px)`;
if (label) label.style.transform = `translate(${dx * strength * 0.5}px, ${dy * strength * 0.5}px)`;
});
});
zone.addEventListener('pointerleave', reset);
reduceMotion.addEventListener('change', reset);
});
})(); Accessibility
Motion, touch and screen readers
Reduced motion
With prefers-reduced-motion: reduce, the script ignores pointer moves and the CSS forces transform: none on the button and its label, so the button never moves. If the setting is switched on while the button is pulled, the script resets it straight away.
Accessibility notes
- The element is a real button, so keyboard focus, Enter and Space work as usual. The movement only happens with a mouse.
- The pull is limited to a fraction of the distance (data-strength), so the button never runs away from the pointer.
- Keep a visible :focus-visible outline. The snippet includes one.
- The wrapper's padding widens the area that reacts to the pointer without changing your layout, because the negative margin cancels it out.
Site builders
Webflow, Framer and React
Webflow
In Webflow, put the CSS inside a <style> tag and the JavaScript inside a <script> tag in the page's custom code (Before </body> tag), or site-wide in Site settings > Custom code. Wrap your button in a div with the class magnetic and give the button and its text the classes magnetic-button and magnetic-label. A Link Block works as well as a button.
Framer
Framer can do a similar effect with its own interactions, but they follow hover, not the pointer's exact position. For the true magnetic pull, make a code component that renders the wrapper and button and runs this script in useEffect.
React
Turn it into a <Magnetic> component: put a ref on the wrapper, attach the listeners in useEffect, and clean them up on unmount. Pass strength as a prop. Keep the transforms in style properties set through refs, not React state, so the button does not re-render on every pointer move.
FAQ
Questions
- Does the magnetic effect hurt usability?
- Not with a gentle strength like 0.3. The button moves toward the pointer, never away, so it is easier to hit, not harder.
- Can I use it on links?
- Yes. Replace the button with an a element that has the class magnetic-button. Keep a real href so it still works without JavaScript.
- How do I make the pull stronger?
- Raise data-strength on the wrapper, for example to 0.45, and widen the padding for a bigger field.