
The layered button
Buttons, Hover, Physics
Free
In the minimal style: who a task belongs to. An empty seat drawn by a dashed circle and, under a hairline, the team in a row of initials. The face chosen with a click, a tap or Enter rises out of the row, flies into the seat filling with ink, lands with a small squash and the blue ring draws round it from the side it came from, while the name beside it takes weight letter by letter. It can also be dragged there by hand: near the seat a magnet pulls it in. The row closes the gap one face after another.
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 avatar to assign</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- who a task belongs to: an empty seat drawn with a dashed hairline, and under the rule the team in a row of
initials. A face chosen (a click, a tap, Enter, or dragged by hand) lifts out of the row, flies on an arc into
the seat filling with ink, lands with a small squash and the seat's ring draws round it in the accent; the name
beside the seat takes weight letter by letter. The faces are made by the script from PEOPLE -->
<div class="assign" role="group" aria-labelledby="assign-label" data-assign data-autoclic>
<div class="seat" aria-hidden="true" data-seat>
<svg class="seat__ring" viewBox="0 0 76 76" data-ring>
<circle class="seat__dash" cx="38" cy="38" r="36" data-dash/>
<circle class="seat__solid" cx="38" cy="38" r="36" data-solid/>
</svg>
<svg class="seat__plus" viewBox="0 0 16 16" data-plus><path d="M8 2.5v11M2.5 8h11"/></svg>
</div>
<div class="who">
<span class="who__label" id="assign-label" data-label>Assignee</span>
<span class="who__name" aria-hidden="true" data-name></span>
<span class="who__role" aria-hidden="true" data-role></span>
</div>
<span class="assign__rule" aria-hidden="true" data-rule></span>
<div class="team" role="group" aria-label="Team" data-team></div>
<p class="sr" aria-live="polite" data-live></p>
</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>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.
31 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The avatar to assign” from Animorio and fit it into my siteThe whole component is a box in pixels worked out by the script from ROW_SIZE, SEAT_SIZE and GAP: the seat at the top left, the name beside it, the rule and the row underneath. Every face is an absolutely positioned button, the size of the seat and shrunk in the row with its scale (ROW_SIZE / SEAT_SIZE), so in the seat the initials are crisp. x, y, scale and rotation reach the face through quickSetter, from damped springs integrated by hand on the GSAP ticker (acceleration −k·distance − 2ζ√k·speed) with the step capped at 1/30 of a second and split into steps of 1/240. When everything is still the loop takes itself off the ticker. On resize the measures are worked out again and the springs carry the faces to their new places.
The flight into the seat is a quadratic Bézier curve run by a GSAP tween: the control point sits between the midpoint and the corner (above the face, at the height of the seat), by as much as ARC says. With ARC near 1 the face rises straight out of the row, without passing over its neighbours, and then glides into the seat; going home it does the opposite, slides out sideways and drops into its place. The scale grows along the way with a little swell in the middle (a sine), the face leans a few degrees into the flight and a soft shadow comes and goes with keyframes. During the flight the name beside the seat steps aside, because the flight passes right where it is written: two things are never on top of each other.
The black ground of the chosen face is a circle with clip-path: circle() that GSAP takes from 0 to 51 per cent, from the centre out. The initials are written twice: in black on the white face and in white inside the ink, cut by the same circle; so at every moment every part of a letter reads, even while the edge of the ink cuts through it. The weight and width of the initials (the wght and wdth axes of Bricolage Grotesque, in CSS variables on the button) change during the flight: in the seat they are fuller and narrower, like a monogram. On arrival a small squash (SQUASH) springs back with an elastic ease.
With Pointer Events a face is dragged after 6 pixels of movement (before that it is a click); touch-action: none on the faces keeps the screen still under the finger. The face follows the pointer on a stiffer spring, grows a little and leans with its speed. Near the seat, within MAGNET pixels, the point it is heading for is pulled towards the centre of the seat with a weight of (1 − d / MAGNET)² · 0.6, so the face lets itself be caught. Near enough, the dashed circle opens and turns black and the name appears beside it: let go there it goes into the seat, far away it goes back into the row. The click that follows a drag is ignored.
The name is made of spans, one per letter, with font-variation-settings written from two CSS variables that GSAP runs: it comes in with wdth from 75 to 100 and a small overshoot, then takes weight from 330 to 640 one letter after another (STAGGER); the old one leaves upwards before the new one comes in. The ring is a single stroke of an SVG circle with stroke-dasharray L, C − L and stroke-dashoffset L / 2, turned towards the point the face arrives from: it grows both ways until it closes. The row takes its new places in turn: closing a gap the neighbours go first, opening one the farthest go first, so nobody bumps into the face beside it. In a swap the old face goes home first and the new one leaves when the other is on its way down.
A click from a script on the component (detail 0, the library’s preview) plays a whole round on a GSAP timeline: a face looked at and chosen, a second one taking its place, the seat freed, a third face carried by a pretend hand over the row to the magnet, and the seat empty again, as it began. Real clicks and the keyboard stop the round.
| Name | File | Default | What it changes |
|---|---|---|---|
PEOPLE | script.js | [['Maya Ortiz', 'Design lead'], …] | The team: each person’s name and role. The initials come from the name. |
START | script.js | -1 | Who sits in the seat when the page opens: their place in PEOPLE counting from 0, or −1 for nobody. |
ROW_SIZE | script.js | 44 | How big a face is in the row, in pixels. |
SEAT_SIZE | script.js | 56 | How big the face in the seat is, in pixels. |
GAP | script.js | 12 | The space between two faces in the row, in pixels. |
FLIGHT | script.js | 0.64 | How long the flight to the seat lasts, in seconds (a short flight takes less). |
ARC | script.js | 0.9 | How square the flight is: towards 1 the face rises straight out of the row and then goes across, lower it cuts diagonally. |
SPRING | script.js | [380, 0.7] | The faces’ spring: how stiff it is and how much it is held back (1 does not bounce). |
MAGNET | script.js | 86 | From how many pixels away the seat starts pulling in a dragged face. |
SQUASH | script.js | 0.07 | How much the face flattens when it lands. |
STAGGER | script.js | 0.024 | The delay from one letter of the name to the next, in seconds. |
EMPTY | script.js | Unassigned | The word beside the empty seat. |
Wherever a person is chosen for something: who a task belongs to in a project app, who looks after a client or an order, who answers a message in a shared inbox, who is on shift, who reviews a document. It also works as the choice of an owner in a form, with the people a click away instead of a dropdown.
The component is 300 pixels wide and fits a 390 screen without touching the edges; with more people or bigger faces the row draws closer to fit. Touching a face shows its name beside the seat while the finger is down, like hovering with the pointer; when the finger lifts the face flies into the seat with the same movement as on a computer. It can be dragged with a finger too: the screen does not scroll while you carry it.
No flights and no springs: the chosen face fades out of the row and reappears in the seat already full of ink, the row rearranges itself at once as soon as the face has faded, the ring fades in instead of drawing and the letters of the name come and go with a fade only, without rising and without changing width. The dashed circle does not open when a dragged face comes near: it only turns black.
GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP 28 KB.
Tested on 18 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.