
The copy button
Buttons, Text, SVG, Physics
NewFree
In the minimal style: the email field of a newsletter with no box round it, and under it a real string held between two small pegs. Every key plucks it right under the caret, deleting plucks it more softly; a valid address tightens it, straightens it and turns it orange, a wrong one leaves it slack and low. Sent, the string winds in, the letters of the confirmation rise out of it and its end bends into a tick.
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 plucked-string field</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/full.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the email field of a newsletter sign-up whose underline is a real string: every key plucks it under the caret,
a valid address tunes it up into the accent, an address that is not right yet leaves it slack, and sending
shortens it into the confirmation -->
<form class="pluck" action="#" novalidate data-pluck data-autoclic>
<div class="pluck__row">
<div class="pluck__field" data-field>
<label class="pluck__label" for="pluck-email" data-label>Email address</label>
<input class="pluck__input" id="pluck-email" name="email" type="text" inputmode="email" autocomplete="email" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="send" maxlength="254" aria-describedby="pluck-note" aria-invalid="false" data-input>
<span class="pluck__caret" aria-hidden="true" data-caret></span>
<p class="pluck__done" aria-hidden="true" data-done></p>
</div>
<button class="send" type="submit" data-send>
<span class="send__bg" aria-hidden="true"></span>
<span class="send__label">Subscribe</span>
</button>
</div>
<div class="pluck__lane" aria-hidden="true" data-lane>
<svg class="pluck__string" data-svg>
<path class="string__pegs" data-pegs/>
<path class="string__line" pathLength="1" data-line/>
<path class="string__tick" pathLength="1" data-tick/>
</svg>
</div>
<div class="pluck__note" id="pluck-note" data-note><span class="note__line">One record a week, picked by ear.</span></div>
<span class="sr" aria-live="polite" data-live></span>
</form>
</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.
22 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 plucked-string field” from Animorio and fit it into my siteThe line is the one-dimensional wave equation solved on 96 points with finite differences (the leapfrog method: the new displacement comes from the current one, the previous one and the curvature between neighbours), with both ends held still. The wave speed comes from the note (PITCH, TAUT, SLACK) and from the string’s full length; the time step follows it, so the ratio between speed, step and point spacing stays at 0.5 and the string never blows up, even short and high. A damping DAMP quiets everything, and a viscous term (the curvature now minus the curvature before) quiets the short waves first: a pluck starts as two sharp bends and ends as the slow swing of the whole string.
The caret’s place is measured with measureText on a canvas in the input’s font, less the input’s scroll. A pluck is a finger pulling the string into a triangle with its corner at that place, in 12 thousandths of a second, and letting go: the same displacement is added to the state now and to the state before, so the string starts at rest. A key is worth KEY pixels upwards, a deletion KEY times SOFT downwards; an address pasted or filled in by the browser is a run of plucks from left to right on a GSAP timeline. The pointer passing over the line plucks it with its own speed.
Note, sag, thickness and colour sit in one object that GSAP takes from state to state. A valid address: the note rises to TAUT, the thickness goes to 1.6 pixels, the colour to the accent, and for a moment the damping is five times stronger, so the string straightens as if pulled by a hand. An address that is not right: the note drops to SLACK and the string sags by SAG pixels with a parabola added to the points (elastic.out), then a pluck in the middle. The displacement is drawn through a hyperbolic tangent, which keeps it within REACH pixels with no hard edge.
On every frame the SVG path is rewritten with quadratic curves through the points, and the pegs follow the ends. The loop runs on the GSAP ticker, its step capped at 1/30 of a second; it falls asleep once the string is still and nothing is animating its state (gsap.isTweening), and every tween that changes it wakes it when it starts. The label rises on three axes of Roboto Flex: size from 20 to 12 pixels (with the opsz axis following it by itself), weight from 380 to 620, width from 100 to 114, with a one-pixel bounce.
On sending, the right end of the string slides to the end of the confirmation (measured beforehand, letter by letter, at its final weight) in WAIT seconds, with small plucks near the tip, and the shortened string sings higher because the wave speed does not change. The letters of DONE sit in boxes as wide as the finished letter; inside, the glyph rises from under the string at weight 260 and width 84 and settles at 560 and 100 with back.out, plucking the string under it. The tick is a path that starts at the string’s end and draws itself with stroke-dashoffset. A click from a script on the form (detail 0, the library’s preview) plays one whole round on a GSAP timeline, without taking the focus and with a drawn caret.
| Name | File | Default | What it changes |
|---|---|---|---|
PITCH | script.js | 3 | The note of the string at rest, in swings per second: higher, it vibrates faster. |
TAUT | script.js | 6.5 | The note once the address is valid: the string tuned up. |
SLACK | script.js | 1.4 | The note when the address is not right yet: low and loose. |
SAG | script.js | 4 | How far the slack string sags in the middle, in pixels. |
KEY | script.js | 5 | How hard a key plucks the string, in pixels. |
SOFT | script.js | 0.4 | How hard a deleted letter plucks it, as a share of a key. |
DAMP | script.js | 2.2 | How quickly the string quiets down: 1 rings for long, 6 is a muted string. |
REACH | script.js | 9 | The farthest the string can swing off the line, in pixels. |
STRUM | script.js | true | Whether the pointer crossing the string plucks it too. |
WAIT | script.js | 0.7 | How many seconds the subscription takes, while the string winds in. |
DONE | script.js | You’re on the list. | The line the string turns into once the address is sent. |
In a newsletter sign-up, at the end of an article or a landing page, on a music or sound site: wherever the form is a single one, needs little room and has to be noticed without a box. The string says by itself whether the address is ready, before the button is even pressed.
The form takes the width of the screen (358 pixels on a 390 phone), the field stays 60 tall, the address drops to 18 pixels and the pill to 40. A tap opens the email keyboard, with the @ and the dot, and the Enter key says Send; every letter plucks the string as on a computer. A finger sliding across the line plucks it like the pointer. An address longer than the field scrolls, and the pluck follows the caret while it scrolls.
The string does not vibrate or wind in: it stays straight, and only its colour and thickness change with a fade (grey at rest, black while typing, orange with a valid address). With an address that is not right the sag appears already in place, without swinging. The label fades out and back in at the top, without rising; on sending the field and the confirmation swap with a fade, the string already short and the tick already drawn.
GSAP 3.15.0, Roboto Flex (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.
Tested on 16 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.