Skip to content
animorio
IT
All components

The magnetic button

Buttons, HoverGSAPMediumCompressed weight: Demo 4.8 KBGSAP 28 KBFreeDownload the zip

A big round button that comes to meet the pointer: when the pointer comes close the button leans towards it, the label inside leans a little further and a warm light turns on right under it; when it leaves, the button springs back to its place. A ring of words turns around it, faster while the magnet holds.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The magnetic button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/manrope@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <div class="magnets" data-magnets>
      <!-- the big magnet: a round button with a ring of words turning around it, faster while the pointer holds it -->
      <div class="reel" data-reel>
        <svg class="reel__ring" data-ring viewBox="0 0 300 300" aria-hidden="true">
          <defs><path id="ring-path" d="M150 150m-128 0a128 128 0 1 1 256 0a128 128 0 1 1-256 0"/></defs>
          <text xml:space="preserve"><textPath href="#ring-path" textLength="800">WATCH THE FILM  /  TWO MINUTES  /  TURN THE SOUND ON  /  </textPath></text>
        </svg>
        <div class="magnet magnet--round" data-magnet data-pull="1">
          <span class="magnet__ripple" aria-hidden="true"></span>
          <a class="magnet__body round" href="#" data-body aria-label="Play the film">
            <span class="magnet__glow" aria-hidden="true"></span>
            <span class="magnet__label round__label" data-label>
              <svg class="round__play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.5v13l10.5-6.5z"/></svg>
              <span>Play<br>the film</span>
            </span>
          </a>
        </div>
      </div>

      <!-- two smaller magnets: they follow the pointer half as much as the big one -->
      <div class="pills">
        <div class="magnet" data-magnet data-pull="0.55">
          <span class="magnet__ripple" aria-hidden="true"></span>
          <a class="magnet__body pill pill--full" href="#" data-body>
            <span class="magnet__glow" aria-hidden="true"></span>
            <span class="magnet__label" data-label>Our work</span>
          </a>
        </div>
        <div class="magnet" data-magnet data-pull="0.55">
          <span class="magnet__ripple" aria-hidden="true"></span>
          <a class="magnet__body pill pill--line" href="#" data-body>
            <span class="magnet__glow" aria-hidden="true"></span>
            <span class="magnet__label" data-label>Say hello</span>
          </a>
        </div>
      </div>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

20 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The magnetic button” from Animorio and fit it into my site

What it does

A big cream round button on a black background, Play the film, with a ring of words turning around it: WATCH THE FILM, TWO MINUTES, TURN THE SOUND ON. Below there are two pills, Our work filled and Say hello with just its outline. When the pointer comes within 90 pixels of a button’s edges, the button follows it: it moves towards it by 40% of the distance from its centre, the label inside by another 22%, and inside the button an amber light turns on right under the pointer. The pills do the same at a little over half. When the pointer leaves, the button goes back to its place with an elastic bounce. The ring makes a turn every 26 seconds and, while the magnet holds the big button, speeds up to a turn every 7. A press shrinks the button to 92% and an amber ring leaves it, growing and fading. When they arrive the big button grows into place and the pills rise after it.

How it works

The zone that never moves

Each magnet is a wrapper that never moves, holding the button’s body, which does. The zone that catches the pointer is the wrapper’s rectangle widened by REACH pixels on every side: if it were the button itself, moving towards the pointer would drag the zone along and never let the pointer go.

The chase

On every mouse move each magnet measures the distance between the pointer and its wrapper’s centre and multiplies it by PULL for the body, by LABEL for the label, and by the button’s data-pull (1 for the big one, 0.55 for the pills). The body does not jump there: on every frame of GSAP’s ticker it covers a share of the remaining way, 1 − e^(−dt / (FOLLOW / 4)), which does not depend on how many frames per second the screen draws. One style write per frame moves the body, and one the label.

The light

Inside the button there is a layer with a radial gradient centred on two CSS variables, --gx and --gy, which the script updates with the pointer’s position as a percentage of the button. The layer turns on with an opacity transition while the magnet holds.

The way back

When the pointer leaves the zone, a tween takes the same numbers to zero in 1.2 seconds with an elastic.out of strength and time SPRING: the button overshoots its place, comes back and settles.

The ring of words

The words sit on a textPath along a circle, with textLength so they cover the whole turn. GSAP turns them forever, one turn every SPIN[0] seconds; while the magnet holds the big button the tween’s timeScale rises to SPIN[0] / SPIN[1] in 0.8 seconds, and goes back to 1 when it lets go.

The press

A press shrinks the body to PRESS in 0.15 seconds and sends out of the wrapper a ring that grows RIPPLE[0] times and fades in RIPPLE[1] seconds, starting from where the body is at that moment. Letting go brings the body back to its size on a spring. With the Tab key the light turns on in the middle of the button, and Enter presses it.

Values to change

NameFileDefaultWhat it changes
PULLscript.js0.4How far a button follows the pointer, as a share of the distance (the pills go a little over half).
LABELscript.js0.22How much further the label inside goes, as a share of the distance.
REACHscript.js90How many pixels around a button the magnet catches the pointer.
FOLLOWscript.js0.6The seconds a button takes to catch up with the pointer.
SPRINGscript.js[1, 0.3]The spring when the pointer lets go: its strength and how quickly it settles.
PRESSscript.js0.92How small a button gets under a press.
RIPPLEscript.js[1.6, 0.75]The ring of a press: how much it grows, and its seconds.
SPINscript.js[26, 7]The seconds per turn of the ring of words: at rest and while the magnet holds.

Try them in the Playground

When to use it

For one important action on a calm page: watch the film, write to us, book. The big button wants room around it, at least as much as its catching zone, and should not sit next to other magnets: they would fight over the pointer.

On phones

The big button narrows to the width of the screen and the pills stay below. With no pointer coming close there is no pull: a tap lights the button where the finger touches for half a second, shrinks it and sends out the ring.

With reduced motion

The buttons do not move, a press does not shrink them and the ring of words holds still. The light under the pointer stays, and so does the ring of a press, which appears and fades without growing.

Pitfalls

  • The catching zone must hold still: if you measure the moving button, the button drags the zone along and the pointer can never get free of it.
  • Body and label move with a translate3d written by hand on every frame: a gsap.to on every mouse move would create dozens of tweens per second fighting over the same element.
  • The ring must turn around its centre with transformOrigin: svgOrigin is only for elements inside an svg, and on the outer svg it turns it around the wrong point.
  • The ring’s text has xml:space="preserve": without it, the double spaces around the slashes shrink to one and the line no longer spreads over the turn as planned.

Libraries

GSAP 3.15.0, Manrope (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 4.8 KB; the libraries: GSAP 28 KB.

Testing

Tested on 2 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components