Skip to content
animorio
IT
All components

The login form with tightening screws

Forms, SVG, PhysicsGSAPMediumCompressed weight: Demo 8.5 KBGSAP 28 KBFreeDownload the zip

In the minimal style, on white: the sign in form of a computer repair shop, a hairline card with email, password and a Sign in button. In its four corners four small steel cross-head screws sit loose and crooked; every character typed turns one of them a little tighter, on a spring, until with a good email and a long enough password they are all flush, their crosses lined up. Sign in seats the card with a soft click, a light runs over the heads and You’re in arrives; a password that is too short loosens the screws again and the card trembles.

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 login form with tightening screws</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the sign in card of a computer repair shop, on white. Four small cross-head screws sit loose and crooked in its
         corners; every character typed turns one of them a little tighter, and once the email and the password are good
         all four are flush and lined up. Sign in seats the panel with a soft click; a short password loosens them again -->
    <form class="panel" data-panel data-autoclic novalidate>
      <span class="screw screw--tl" data-screw aria-hidden="true">
        <span class="screw__shadow" data-shadow></span>
        <svg class="screw__head" viewBox="0 0 20 20" data-head>
          <defs>
            <radialGradient id="metal0" cx="35%" cy="30%" r="75%">
              <stop offset="0" stop-color="#fbfbfa"/>
              <stop offset=".55" stop-color="#cfcfcb"/>
              <stop offset="1" stop-color="#9c9c97"/>
            </radialGradient>
            <clipPath id="cap0"><circle cx="10" cy="10" r="8.4"/></clipPath>
          </defs>
          <circle cx="10" cy="10" r="8.4" fill="url(#metal0)"/>
          <circle cx="10" cy="10" r="8.4" fill="none" stroke="#8d8d88" stroke-width=".7"/>
          <circle cx="10" cy="10" r="6.9" fill="none" stroke="#ffffff" stroke-opacity=".55" stroke-width=".5"/>
          <g data-slot>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#ffffff" stroke-opacity=".8" stroke-width="2.2" stroke-linecap="round" transform="translate(.35 .45)"/>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#55554f" stroke-width="1.9" stroke-linecap="round"/>
          </g>
          <g clip-path="url(#cap0)"><rect class="screw__glint" data-glint x="-14" y="-4" width="5" height="28" fill="#ffffff" opacity="0" transform="rotate(28 10 10)"/></g>
          <ellipse cx="7.3" cy="6.4" rx="2.6" ry="1.5" fill="#ffffff" opacity=".7" transform="rotate(-32 7.3 6.4)"/>
        </svg>
      </span>
      <span class="screw screw--tr" data-screw aria-hidden="true">
        <span class="screw__shadow" data-shadow></span>
        <svg class="screw__head" viewBox="0 0 20 20" data-head>
          <defs>
            <radialGradient id="metal1" cx="35%" cy="30%" r="75%">
              <stop offset="0" stop-color="#fbfbfa"/>
              <stop offset=".55" stop-color="#cfcfcb"/>
              <stop offset="1" stop-color="#9c9c97"/>
            </radialGradient>
            <clipPath id="cap1"><circle cx="10" cy="10" r="8.4"/></clipPath>
          </defs>
          <circle cx="10" cy="10" r="8.4" fill="url(#metal1)"/>
          <circle cx="10" cy="10" r="8.4" fill="none" stroke="#8d8d88" stroke-width=".7"/>
          <circle cx="10" cy="10" r="6.9" fill="none" stroke="#ffffff" stroke-opacity=".55" stroke-width=".5"/>
          <g data-slot>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#ffffff" stroke-opacity=".8" stroke-width="2.2" stroke-linecap="round" transform="translate(.35 .45)"/>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#55554f" stroke-width="1.9" stroke-linecap="round"/>
          </g>
          <g clip-path="url(#cap1)"><rect class="screw__glint" data-glint x="-14" y="-4" width="5" height="28" fill="#ffffff" opacity="0" transform="rotate(28 10 10)"/></g>
          <ellipse cx="7.3" cy="6.4" rx="2.6" ry="1.5" fill="#ffffff" opacity=".7" transform="rotate(-32 7.3 6.4)"/>
        </svg>
      </span>
      <span class="screw screw--bl" data-screw aria-hidden="true">
        <span class="screw__shadow" data-shadow></span>
        <svg class="screw__head" viewBox="0 0 20 20" data-head>
          <defs>
            <radialGradient id="metal2" cx="35%" cy="30%" r="75%">
              <stop offset="0" stop-color="#fbfbfa"/>
              <stop offset=".55" stop-color="#cfcfcb"/>
              <stop offset="1" stop-color="#9c9c97"/>
            </radialGradient>
            <clipPath id="cap2"><circle cx="10" cy="10" r="8.4"/></clipPath>
          </defs>
          <circle cx="10" cy="10" r="8.4" fill="url(#metal2)"/>
          <circle cx="10" cy="10" r="8.4" fill="none" stroke="#8d8d88" stroke-width=".7"/>
          <circle cx="10" cy="10" r="6.9" fill="none" stroke="#ffffff" stroke-opacity=".55" stroke-width=".5"/>
          <g data-slot>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#ffffff" stroke-opacity=".8" stroke-width="2.2" stroke-linecap="round" transform="translate(.35 .45)"/>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#55554f" stroke-width="1.9" stroke-linecap="round"/>
          </g>
          <g clip-path="url(#cap2)"><rect class="screw__glint" data-glint x="-14" y="-4" width="5" height="28" fill="#ffffff" opacity="0" transform="rotate(28 10 10)"/></g>
          <ellipse cx="7.3" cy="6.4" rx="2.6" ry="1.5" fill="#ffffff" opacity=".7" transform="rotate(-32 7.3 6.4)"/>
        </svg>
      </span>
      <span class="screw screw--br" data-screw aria-hidden="true">
        <span class="screw__shadow" data-shadow></span>
        <svg class="screw__head" viewBox="0 0 20 20" data-head>
          <defs>
            <radialGradient id="metal3" cx="35%" cy="30%" r="75%">
              <stop offset="0" stop-color="#fbfbfa"/>
              <stop offset=".55" stop-color="#cfcfcb"/>
              <stop offset="1" stop-color="#9c9c97"/>
            </radialGradient>
            <clipPath id="cap3"><circle cx="10" cy="10" r="8.4"/></clipPath>
          </defs>
          <circle cx="10" cy="10" r="8.4" fill="url(#metal3)"/>
          <circle cx="10" cy="10" r="8.4" fill="none" stroke="#8d8d88" stroke-width=".7"/>
          <circle cx="10" cy="10" r="6.9" fill="none" stroke="#ffffff" stroke-opacity=".55" stroke-width=".5"/>
          <g data-slot>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#ffffff" stroke-opacity=".8" stroke-width="2.2" stroke-linecap="round" transform="translate(.35 .45)"/>
            <path d="M10 5.1v9.8M5.1 10h9.8" stroke="#55554f" stroke-width="1.9" stroke-linecap="round"/>
          </g>
          <g clip-path="url(#cap3)"><rect class="screw__glint" data-glint x="-14" y="-4" width="5" height="28" fill="#ffffff" opacity="0" transform="rotate(28 10 10)"/></g>
          <ellipse cx="7.3" cy="6.4" rx="2.6" ry="1.5" fill="#ffffff" opacity=".7" transform="rotate(-32 7.3 6.4)"/>
        </svg>
      </span>
      <div class="panel__body" data-body>
        <p class="brand" data-part><span data-brand>Bitfix repairs</span></p>
        <h1 class="title" data-part data-title>Sign in</h1>
        <p class="sub" data-part data-sub>Check on your repair</p>

        <label class="field" data-field="email" data-part>
          <span class="field__label">Email</span>
          <span class="field__box">
            <input class="field__input" type="email" name="email" autocomplete="email" spellcheck="false" data-input="email" placeholder="you@example.com">
          </span>
          <span class="field__line" aria-hidden="true"><span data-line></span></span>
        </label>

        <label class="field" data-field="pass" data-part>
          <span class="field__label">Password</span>
          <span class="field__box">
            <input class="field__input" type="password" name="password" autocomplete="current-password" data-input="pass" placeholder="At least 6 characters">
            <button class="eye" type="button" data-eye aria-label="Show password" aria-pressed="false">
              <svg viewBox="0 0 24 24" aria-hidden="true">
                <path class="eye__lid" data-lid d="M2.8 12c2.4-3.9 5.5-5.8 9.2-5.8s6.8 1.9 9.2 5.8c-2.4 3.9-5.5 5.8-9.2 5.8S5.2 15.9 2.8 12Z"/>
                <circle class="eye__ball" data-ball cx="12" cy="12" r="2.6"/>
                <path class="eye__slash" data-slash d="M4.5 19.5 19.5 4.5"/>
              </svg>
            </button>
          </span>
          <span class="field__line" aria-hidden="true"><span data-line></span></span>
          <span class="field__msg" data-msg aria-live="polite"></span>
        </label>

        <button class="go" type="submit" data-go>
          <span class="go__bg" aria-hidden="true"></span>
          <span class="go__text" data-go-text>Sign in</span>
        </button>
      </div>

      <div class="done" data-done aria-hidden="true">
        <svg class="done__tick" viewBox="0 0 40 40" aria-hidden="true">
          <circle cx="20" cy="20" r="17" data-ring/>
          <path d="M12.5 20.5 18 26l10-11.5" data-tick/>
        </svg>
        <p class="done__title" data-done-title>You’re in</p>
        <p class="done__sub" data-done-sub>Your laptop is on the bench.</p>
      </div>
      <p class="sr" aria-live="polite" data-live></p>
    </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.

24 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 login form with tightening screws” from Animorio and fit it into my site

What it does

On a white background a white card with a hairline outline and a soft shadow: at the top the small name of the shop, Bitfix repairs, then Sign in and Check on your repair; below, two labelled fields, Email and Password, each on a thin line, the eye button that shows the password and a black Sign in pill. In the four corners four steel cross-head screws, each in its recess: they drop in turning and stay loose, a little out of the card, with long shadows and their crosses turned every which way. Typing the email tightens the two top screws, one after the other, a small turn on a spring for every character; the password does the bottom ones. When a screw goes flush its head gives by a pixel and comes back. Sign in with everything right seats the card a pixel lower with a click, every head takes the press and a light runs across them one after the other; the fields rise away and a circle draws itself with a vermilion tick, You’re in and Your laptop is on the bench. With a password shorter than six characters the screws work loose again, each by its own amount, the card trembles and the error shows in vermilion under the password. In the preview a pretend hand types on its own: the email, every other time a password that is too short, then the right one.

How it works

A screw in three layers

Each screw is a blurred disc for the shadow, the SVG head with a radial steel gradient and, inside the head, the group of the cross, which is the only part that turns. The highlight on the head sits outside that group: the light stays still while the screw turns under it, and that is what makes it read as metal.

How tight

Each screw has a number p from 0 to 1. At 0 the cross is turned LOOSE degrees anticlockwise (each by its own share), the head is 8% larger and shifted towards the light, the shadow far away; at 1 everything is flush. GSAP takes p to its new value with back.out(2.2), so every turn goes a touch past the mark and settles.

Characters tighten

The email is worth 0 to 0.8 counting characters up to sixteen, and 1 only when it is a real address; the password is worth its length over MIN_PASS. Each pair of screws splits its value in half: the first tightens up to 0.5, the second from 0.5 to 1, so they tighten one after the other.

The click and the refusal

A good Sign in: the card drops 1.2 pixels in 7 hundredths and comes back with back.out(3), every head goes from 0.92 to its size and a white band, clipped to the head’s circle, runs across it. A bad one: every p drops by about 0.4 with power3.out and the card trembles, dying away with elastic.out.

Accessible

It is a real form with two labelled inputs, the eye button with aria-pressed and an aria-live line that reads the error or the sign in. The preview’s pretend hand stops as soon as you really touch or type. With reduced motion the screws change without turning and the tick fades in.

Values to change

NameFileDefaultWhat it changes
BRANDscript.jsBitfix repairsThe small name of the shop over the title.
TITLEscript.jsSign inThe title of the card and the button.
SUBscript.jsCheck on your repairThe line under the title.
DONE_TITLEscript.jsYou’re inThe title once signed in.
DONE_SUBscript.jsYour laptop is on the bench.The line once signed in.
MIN_PASSscript.js6How many characters the password needs.
LOOSEscript.js150How many degrees a loose screw is turned.
LIFTscript.js1.6How far a loose screw stands out, in pixels.
TURNscript.js0.42The seconds of one turn of a screw.
SHAKEscript.js4How far the card trembles when refused, in pixels.

Try them in the Playground

When to use it

For the sign in of a support site, a workshop, a repair service or any customer area that wants to say «all fixed» with a hands-on gesture.

On phones

The card narrows to 16 pixels from the edges and the vertical spacing is measured on the screen height too, so it fits whole even in a short box. A tap types and presses like the mouse; the screw turning under the pointer happens only with a mouse.

With reduced motion

The screws go from loose to tight without turning, the card does not tremble, the fields and the tick fade in.

Pitfalls

  • The highlight must not turn with the screw: inside the rotating group the head looks like plastic. It sits outside, and only the cross turns.
  • The screws tighten in pairs: if all four followed the same value, every character would turn everything at once and you could not read the count.
  • The cross is symmetric every 90 degrees: a quarter turn is invisible. So the final click is not a turn but the head giving and the light running over it.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

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

Testing

Tested on 24 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