Skip to content
animorio
IT
All components

The plucked-string field

Forms, Physics, Text, SVGGSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

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.

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 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>

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.

22 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 plucked-string field” from Animorio and fit it into my site

What it does

On a white page the words Email address in grey, large, resting where the address will go; on the right the black Subscribe pill; under it a thin grey line held between two small upright pegs, and lower down One record a week, picked by ear. Tapping the field the words rise, grow smaller, take weight and widen a little, and leaving the line they set it trembling; the string turns black. Every letter typed plucks the string at the caret: the string bends into a triangle with its corner under the letter, lets go and swings, first with two bends running to the pegs and then with a slow ripple that dies away. Deleting plucks it more softly and downwards; pasting an address plays it from end to end. The pointer crossing the string plucks it too. As soon as the address is valid the string tightens: the swing stops at once, the note rises, the line grows thicker and turns orange. Pressing Subscribe with an address that is not right, the string goes slack: it sags, swings low and slow and dies away, and under it shows what is missing (Add the ending, like .com.); as soon as the address is right again it tightens and the message goes. With a valid address, instead, the address and the button rise and fade, the string winds in towards its left peg trembling near its end, like a peg being turned, stops a pixel past its mark and comes back; then the letters of You’re on the list. rise out of the string one after the other, thin and narrow, take weight as they land and each one plucks the string where it lands. At the end the tip of the string bends into a tick and under it reads Sent to and the address, with Change to go back to the field: the letters drop back into the string, the string grows long again with a little bounce and the address comes back to its place.

How it works

A real string: the wave equation

The 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 pluck under the caret

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.

The tension: taut, slack, at rest

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.

The line redrawn, and the loop that sleeps

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.

Sending, the confirmation and the preview

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.

Values to change

NameFileDefaultWhat it changes
PITCHscript.js3The note of the string at rest, in swings per second: higher, it vibrates faster.
TAUTscript.js6.5The note once the address is valid: the string tuned up.
SLACKscript.js1.4The note when the address is not right yet: low and loose.
SAGscript.js4How far the slack string sags in the middle, in pixels.
KEYscript.js5How hard a key plucks the string, in pixels.
SOFTscript.js0.4How hard a deleted letter plucks it, as a share of a key.
DAMPscript.js2.2How quickly the string quiets down: 1 rings for long, 6 is a muted string.
REACHscript.js9The farthest the string can swing off the line, in pixels.
STRUMscript.jstrueWhether the pointer crossing the string plucks it too.
WAITscript.js0.7How many seconds the subscription takes, while the string winds in.
DONEscript.jsYou’re on the list.The line the string turns into once the address is sent.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The first pluck was a bell-shaped force held for 34 thousandths of a second: with waves at almost 8000 pixels a second the wave ran off before the finger had finished pulling, and a 5-pixel key moved the string by less than one. Now the finger pulls a whole triangle, with its corner under the caret, in 12 thousandths, like a real plucked string.
  • The loop counted the tweens running to know when to sleep, but a timeline stopped halfway does not tell its tweens: the count never got back to zero and the loop never slept. Now it checks gsap.isTweening on the string’s state, and every tween wakes it when it starts.
  • Stopping the label’s tweens by property name does not stop those of CSS variables: going straight from the field to the button the label came back down large in position but stayed small and bold. Now the label’s tween is kept aside and stopped whole. And stopping every tween of the label, when the fonts arrived, killed its fade-in and left it invisible.
  • When the lines under the field swapped, the new one came in while the old one was still fading and for a moment they read on top of each other: the new one now waits for the old one to have gone.

Libraries

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.

Testing

Tested on 16 October 2026

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

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