Interaction blueprint
Magnetic CTA
A restrained pointer pull makes an editorial call to action feel responsive, not slippery.
Build it
Choose a target and adapt the source to your interface.
<span class="magnet-region"
><button class="magnet-control" type="button">
<span>Explore</span>
</button></span
>
<script>
const region = document.querySelector('.magnet-region');
const control = region.querySelector('button');
const reset = () => {
control.style.setProperty('--x', '0px');
control.style.setProperty('--y', '0px');
};
region.addEventListener('pointermove', (event) => {
if (
event.pointerType !== 'mouse' ||
matchMedia('(prefers-reduced-motion: reduce)').matches
)
return;
const bounds = region.getBoundingClientRect();
const x =
Math.max(
-1,
Math.min(
1,
(event.clientX - bounds.left - bounds.width / 2) / (bounds.width / 2),
),
) * 10;
const y =
Math.max(
-1,
Math.min(
1,
(event.clientY - bounds.top - bounds.height / 2) /
(bounds.height / 2),
),
) * 10;
control.style.setProperty('--x', x + 'px');
control.style.setProperty('--y', y + 'px');
});
region.addEventListener('pointerleave', reset);
region.addEventListener('pointercancel', reset);
</script>
<style>
.magnet-region {
display: inline-block;
padding: 16px;
}
.magnet-control {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
min-width: 140px;
padding: 12px 20px;
border: 0;
border-radius: 6px;
background: #175bc5;
color: white;
font: 14px system-ui;
cursor: pointer;
transform: translate(var(--x, 0px), var(--y, 0px));
transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1);
}
.magnet-control span {
display: block;
transform: translate(
calc(var(--x, 0px) * -0.2),
calc(var(--y, 0px) * -0.2)
);
transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1);
}
.magnet-control:focus-visible {
outline: 2px solid #175bc5;
outline-offset: 5px;
}
@media (prefers-reduced-motion: reduce) {
.magnet-control,
.magnet-control span {
transform: none;
transition: none;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
01
Rest: stable CTA.
02
0–120ms: button follows pointer up to 10px.
03
120–160ms: label counterslides slightly; leave springs home.
Motion intent
A restrained pointer pull makes an editorial call to action feel responsive, not slippery.
This is direct manipulation feedback, so it completes within the immediate-response window.
The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.
Motion specification
- Trigger
- Pointer enters the CTA’s attraction radius.
- Timing
- 160ms
- Easing
- cubic-bezier(.16, 1, .3, 1)
Animated properties
translatebackground-colorshadow
Implementation notes
- Keep idle and attracted aligned to the same visual anchor.
- Drive the transition from semantic state, not a timer, when the trigger is interactive.
- Use translate, background-color, shadow and avoid layout work inside the animation loop.
Reduced motion
Swap to the final state instantly, preserving colour, label, and accessible state.
- Use a minimum 44 by 44 pixel interactive target.
- Expose the idle and attracted states with aria-pressed, aria-expanded, or an updated accessible label.
- Keep visible keyboard focus independent from the animation.
Avoid
- Do not add bounce unless a real object pickup or spring settle is part of the interaction.
- Do not hide the final state from assistive technology.
- Do not make the user wait for animation before the underlying state updates.