Skip to content
animorio
IT
All components

The card field

Forms, SVG, 3D, Physics, TextGSAP, CSS only, CanvasAdvancedCompressed weight: Demo 22 KBGSAP 28 KBFreeDownload the zip

The card fields of a checkout with the card drawn above them: every figure typed is minted into the plastic, standing up bare in relief and then filling with silver or gold, under a small lamp that follows the caret. The first figures choose the network and its material, the light reads the number back from the right like the Luhn check, and for the security code the card turns over.

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 card field</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/hubot-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/martian-mono@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the card fields of a checkout, with the card they describe drawn above them. Every figure typed is struck
         into the plastic and stands up in relief, lit by a small lamp that follows the caret; the first figures
         choose the network and its material, a full number is read by the light from right to left (the Luhn
         check), and the card turns over for the security code. The networks and their marks are invented -->
    <form class="pay" action="#" novalidate data-pay data-autoclic>
      <div class="pay__card" aria-hidden="true" data-stage>
        <div class="card__shadow" data-shadow></div>
        <div class="card is-blank" data-net="blank" data-card>
          <div class="card__face card__face--front" data-face="front">
            <div class="card__mats" data-mats></div>
            <div class="card__lamp" data-lamp></div>
            <div class="card__glint" data-glint></div>
            <svg class="card__art" viewBox="0 0 428 270" data-art>
              <defs>
                <linearGradient id="cc-chip" x1="0" y1="0" x2="1" y2="1">
                  <stop offset="0" stop-color="#f6e2a8"/>
                  <stop offset="0.5" stop-color="#d5ad5c"/>
                  <stop offset="1" stop-color="#a87f37"/>
                </linearGradient>
                <!-- the relief: the alpha of the figures is a height map, lit by a point light (the lamp) -->
                <filter id="cc-emboss" filterUnits="userSpaceOnUse" x="0" y="96" width="428" height="174" color-interpolation-filters="sRGB">
                  <feGaussianBlur in="SourceAlpha" stdDeviation="0.95" result="bump"/>
                  <feDiffuseLighting in="bump" surfaceScale="2.6" diffuseConstant="1.05" lighting-color="#ffffff" result="shade">
                    <fePointLight x="80" y="20" z="220" data-light/>
                  </feDiffuseLighting>
                  <feSpecularLighting in="bump" surfaceScale="2.6" specularConstant="1" specularExponent="16" lighting-color="#ffffff" result="glint">
                    <fePointLight x="80" y="20" z="220" data-light/>
                  </feSpecularLighting>
                  <feComposite in="glint" in2="SourceAlpha" operator="in" result="glintIn"/>
                  <feComposite in="SourceGraphic" in2="shade" operator="arithmetic" k1="0.9" k2="0.42" k3="0" k4="0" result="lit"/>
                  <feComposite in="glintIn" in2="lit" operator="arithmetic" k1="0" k2="0.85" k3="1" k4="0" result="relief"/>
                  <feOffset in="bump" dx="-0.8" dy="1.3" result="off" data-cast/>
                  <feComponentTransfer in="off" result="shadow"><feFuncA type="linear" slope="0.55"/></feComponentTransfer>
                  <feMerge><feMergeNode in="shadow"/><feMergeNode in="relief"/></feMerge>
                </filter>
                <!-- the chip catches the same lamp -->
                <filter id="cc-metal" filterUnits="userSpaceOnUse" x="28" y="72" width="70" height="56" color-interpolation-filters="sRGB">
                  <feGaussianBlur in="SourceAlpha" stdDeviation="0.6" result="bump"/>
                  <feSpecularLighting in="bump" surfaceScale="1.6" specularConstant="0.55" specularExponent="22" lighting-color="#fff4d6" result="glint">
                    <fePointLight x="80" y="20" z="220" data-light/>
                  </feSpecularLighting>
                  <feComposite in="glint" in2="SourceAlpha" operator="in" result="glintIn"/>
                  <feComposite in="glintIn" in2="SourceGraphic" operator="arithmetic" k1="0" k2="0.45" k3="1" k4="0"/>
                </filter>
              </defs>

              <g class="chip" filter="url(#cc-metal)">
                <rect x="34" y="78" width="56" height="42" rx="8" fill="url(#cc-chip)"/>
                <path class="chip__lines" d="M34 92.5h15.5a5 5 0 0 1 5 5v0M34 105.5h15.5a5 5 0 0 0 5-5M90 92.5H74.5a5 5 0 0 0-5 5M90 105.5H74.5a5 5 0 0 1-5-5M54.5 78v9.5M69.5 78v9.5M54.5 120v-9.5M69.5 120v-9.5M54.5 87.5h15v23h-15z"/>
              </g>

              <g class="card__print" data-print>
                <text x="168" y="194">VALID</text>
                <text x="168" y="201.5">THRU</text>
              </g>
              <!-- the Atrium cards print their code on the front, above the number -->
              <text class="card__frontcode" x="394" y="134" text-anchor="end" data-frontcode></text>

              <g class="card__marks" data-marks></g>

              <g class="card__relief" filter="url(#cc-emboss)" data-relief>
                <g class="card__number" data-number></g>
                <g class="card__exp" data-exp></g>
                <g class="card__name" data-name></g>
              </g>
            </svg>
          </div>

          <div class="card__face card__face--back" data-face="back">
            <div class="card__mats" data-mats></div>
            <div class="card__lamp" data-lamp></div>
            <div class="card__glint" data-glint></div>
            <svg class="card__art" viewBox="0 0 428 270">
              <defs>
                <pattern id="cc-panel" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(35)">
                  <rect width="6" height="6" fill="#f1efe9"/>
                  <path d="M0 3h6" stroke="#dcd8ce" stroke-width="1.2"/>
                </pattern>
                <linearGradient id="cc-stripe" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0" stop-color="#2a2a2e"/>
                  <stop offset="0.5" stop-color="#0d0d0f"/>
                  <stop offset="1" stop-color="#1c1c20"/>
                </linearGradient>
              </defs>
              <rect x="0" y="30" width="428" height="48" fill="url(#cc-stripe)"/>
              <rect x="26" y="102" width="272" height="38" rx="3" fill="url(#cc-panel)"/>
              <rect class="card__codebox" x="304" y="102" width="74" height="38" rx="3" data-codebox/>
              <g class="card__code" transform="translate(22.4 0) skewX(-10)" data-code></g>
              <g class="card__small">
                <text x="26" y="184">This card remains the property of the issuer and is used under</text>
                <text x="26" y="196">the cardholder agreement. If you find it, please hand it in at</text>
                <text x="26" y="208">any bank, or cut it in half and throw it away.</text>
              </g>
              <g class="card__marks card__marks--back" data-marks></g>
            </svg>
          </div>
        </div>
      </div>

      <div class="pay__fields">
        <div class="field field--number" data-field="number">
          <input class="field__input field__input--mono" id="cc-number" name="cardnumber" type="text" inputmode="numeric" autocomplete="cc-number" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="1234 1234 1234 1234" aria-describedby="pay-note" data-input="number">
          <label class="field__label" for="cc-number">Card number</label>
          <svg class="field__tick" viewBox="0 0 20 20" aria-hidden="true" data-tick><path d="M4.6 10.4l3.6 3.6 7.2-8"/></svg>
          <span class="field__badge" aria-hidden="true" data-badge></span>
        </div>
        <div class="pay__row">
          <div class="field" data-field="exp">
            <input class="field__input field__input--mono" id="cc-exp" name="cc-exp" type="text" inputmode="numeric" autocomplete="cc-exp" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="MM / YY" aria-describedby="pay-note" data-input="exp">
            <label class="field__label" for="cc-exp">Expiry date</label>
          </div>
          <div class="field field--code" data-field="code">
            <input class="field__input field__input--mono" id="cc-csc" name="cvc" type="text" inputmode="numeric" autocomplete="cc-csc" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="3 digits" aria-describedby="pay-note" data-input="code">
            <label class="field__label" for="cc-csc">Security code</label>
            <span class="field__hint" aria-hidden="true" data-hint>
              <span class="hint__card" data-hint-card>
                <svg class="hint__side hint__side--back" viewBox="0 0 30 20"><rect x="0.75" y="0.75" width="28.5" height="18.5" rx="3"/><path class="hint__stripe" d="M0.75 4.5h28.5v3.6H0.75z"/><rect class="hint__box" x="18" y="11" width="8" height="4.4" rx="1"/></svg>
                <svg class="hint__side hint__side--front" viewBox="0 0 30 20"><rect x="0.75" y="0.75" width="28.5" height="18.5" rx="3"/><rect class="hint__box" x="19" y="4" width="7.5" height="4" rx="1"/><path class="hint__line" d="M4 13.5h18"/></svg>
              </span>
            </span>
          </div>
        </div>
        <div class="field" data-field="name">
          <input class="field__input" id="cc-name" name="ccname" type="text" autocomplete="cc-name" autocapitalize="words" autocorrect="off" spellcheck="false" enterkeyhint="done" maxlength="26" placeholder="As it is printed on the card" aria-describedby="pay-note" data-input="name">
          <label class="field__label" for="cc-name">Name on card</label>
        </div>
      </div>

      <p class="pay__note" id="pay-note" aria-live="polite" data-note>Encrypted and sent straight to your bank.</p>

      <button class="go" type="submit" data-go>
        <span class="go__bg" aria-hidden="true" data-go-bg></span>
        <span class="go__label" data-go-label>Pay</span>
        <span class="go__state" aria-hidden="true" data-wait><svg class="go__spin" viewBox="0 0 24 24" data-spin><circle cx="12" cy="12" r="8.5"/></svg><span>Authorizing</span></span>
        <span class="go__state" aria-hidden="true" data-done><svg class="go__tick" viewBox="0 0 24 24"><path d="M6.2 12.6l3.9 3.9 7.8-8.9"/></svg><span>Paid</span></span>
      </button>
      <span class="sr" aria-live="polite" data-said></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.

46 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 card field” from Animorio and fit it into my site

What it does

On a white background, above the fields, a pale plastic card not yet printed, with its gold chip; under it, four grey fields 60 pixels tall with their labels inside (Card number, then Expiry date and Security code side by side, then Name on card), the line Encrypted and sent straight to your bank. with a padlock, and the black Pay $64.00 button. Tapping a field its label rises, its edge turns black with a halo, and on the card a small lamp goes to light the spot where the figure will land. The network is known from the first figure: a band of light crosses the card and behind it the material appears (with 4 Orbit’s brushed graphite, with 51-55 or 2221-2720 Strata’s waved ink blue, with 34 or 37 Atrium’s squared verdigris, with 6011, 644-649 or 65 Kite’s pearly garnet), the mark settles at the bottom right, and in the field the network’s little card turns like a coin. Every figure is minted: it comes down like a punch, the card gives a little jolt around the spot it was hit, and the figure stands in relief in the plastic’s own colour, then the silver or gold runs into it; typing fast, a trail of bare figures follows the caret and fills behind it. The lamp follows the caret, so the relief catches the light from the right side and a vertical gleam runs across the brushed metal. The number splits by itself into the network’s groups (4-6-5 for Atrium) and once complete the light reads it back from right to left, low and grazing: every second figure gives a little hop, the ones the Luhn check counts twice. If the number adds up, a green tick draws itself in the field and the figures ripple; if not, the figures sink back into the plastic, the field turns red and shakes, and That card number doesn’t add up. Check for a typo. shows below. The date is written 08 / 29, the zero and the slash coming by themselves, and appears on the card by VALID THRU; a date in the past dims the card, which loses its colour and its shine, and the date sinks. For the security code the card turns over on a spring and shows its back, with the magnetic stripe and the signature panel where the code is printed in italics; on Atrium cards the code has four figures and sits on the front, above the number, and the card does not turn. The name is minted in capitals and narrows when it is long. Pressing Pay the label rises and leaves Authorizing with a spinner while the lamp sweeps back and forth over the card; then the button turns green with a tick and Paid, and the figures sink back into the plastic one after the other, all but the last four. After a few seconds the form empties.

How it works

Real fields that format themselves

The four fields are real inputs with their labels, autocomplete cc-number, cc-exp, cc-csc and cc-name (so the browser and password managers fill them) and inputmode numeric for the number keyboard. At every input the number is cleaned and split into the network’s groups, and the caret goes back after the same count of figures it had before. Deleting a space would do nothing, because the space would come straight back: in beforeinput the figure before it is deleted instead. The date adds the zero to a one-figure month and the slash, and also understands 12/2029 pasted in.

The network from the first figures

The rules are the real ones (4, 51-55 and 2221-2720, 34 and 37, 6011, 644-649 and 65), the names and marks are invented. Each network has its length, its groups, the length and place of its code, its silver or gold. The materials are layers stacked in both faces: the new one is uncovered by a CSS gradient mask that slides across in 0.7 seconds, and along the mask’s edge passes a band of light in overlay that hides the cut; the same band changes the foil of every figure it crosses. Numbers of other networks stay on the white card, 16 to 19 figures long, and the Luhn check holds for them too.

The relief is an SVG filter

Figures and letters are SVG texts in a group with the filter: their alpha blurred by a pixel is a height map, lit by feDiffuseLighting and feSpecularLighting with an fePointLight (the lamp), the foil colour is multiplied by the shading, and an feOffset of the same relief makes the shadow, moved away from the lamp. Each figure is a group the script moves, with the letter inside that animates: it comes in with its opacity (which is also the height of the relief) and a scale from 1.3, first in the plastic’s colour and then, over FOIL seconds, in the foil’s. The typeface is Martian Mono: for 19 figures or a long name it narrows on its width axis before it shrinks.

The card and the lamp on springs

On GSAP’s ticker the springs of the card (its two rotations, its depth and its turn) and of the lamp (x, y and height) are worked out by hand, in steps of 4 milliseconds with the frame capped at 1/30 of a second. A minted figure gives the card a push around the spot it hit, the harder the further that spot is from the middle (STRIKE). The lamp goes to the slot of the next figure; its pool on the material is a gradient in soft-light, and on brushed metal a vertical streak in screen, as brushed metal really does. For the code the card turns 180 degrees on the TURN spring, and the face that cannot be seen is also hidden by the script the moment the card is edge on.

Luhn, errors, paying and the preview

A complete number is read back over READ seconds: the lamp drops low and grazing and goes from right to left, and the figures the Luhn check doubles give a little hop as it passes; the verdict comes with the light on the first figure. Errors show when a field is left, or at once when they cannot be right (a month 13, an expired card), and go as soon as the field is right again; the line under the fields is aria-live and the fields get aria-invalid. Pay checks everything, waits WAIT seconds and shows Paid. A click from a script (detail 0, the library’s preview) types without taking the focus, one step per click: the test number, the date, the code, the name, paying, then a number with a typo and its fix.

Values to change

NameFileDefaultWhat it changes
AMOUNTscript.js'$64.00'What the button charges.
STRIKEscript.js3.4How far the card jolts when a figure is minted near its edge, in degrees (0: still).
FOILscript.js0.4The seconds the silver or gold takes to fill a figure just minted.
LAMPscript.js[70, 0.78]The lamp’s spring: how stiff, and how much it is held back (1 = no bounce).
TURNscript.js[95, 0.62]The spring that turns the card over for the security code.
READscript.js0.95The seconds the light takes to read the number from right to left (the Luhn check).
WAITscript.js1.5The seconds the bank takes to answer, in the demo.
HOLDscript.js3.4The seconds the paid card stays before the form empties again.
SAMPLEscript.js'4242 4242 4242 4242'The test number the library’s preview types by itself.
HOLDERscript.js'Ines Okafor'The name the preview types.

Try them in the Playground

When to use it

At the checkout of a shop or a subscription, on the page that adds a card to an account, when booking a hotel or a flight: anywhere a card is typed in and it helps to see it while typing. For a real shop the real networks and their marks go in place of the invented ones, and the real payment in place of the pretend one (the submit function). The data of a real card must go to the payment service, never to your own server; services that give their fields ready-made inside an iframe do not let the page read the figures, and there the card can only follow what those fields report (the network, complete or not, the error).

On phones

The form takes the width of the screen (358 pixels on a 390 phone) and the card keeps its proportions, 358 by 226. The fields stay 60 pixels tall, with 16-pixel text so iPhone does not zoom the page on a tap, and the keyboard is the number one. Tapping a part of the card goes to its field; the lamp that follows the pointer is not there on a phone: it only follows the caret.

With reduced motion

No punches, jolts or springs: the figures fade in already in their colour, the material changes with a fade, the card goes from front to back with a fade instead of turning, the lamp goes straight to its place, the Luhn reading gives its verdict at once and the fields do not shake.

Pitfalls

  • The button’s background, absolutely placed, had ended up in the grid with the labels (grid-area 1 / 1 on every child) and only took the cell inside the padding: the black pill was shorter than the fields and pushed right. The rule now leaves the background out.
  • The figures are moved and animated with GSAP: a transform-origin written in the CSS on an SVG text adds itself to the origin GSAP works out on its own. Each figure is a group GSAP moves, with the letter inside that scales, and no transform-origin in the CSS; for the same reason the italics of the code on the back sit on a group GSAP never touches.
  • The type size written in the CSS on the texts beat the one the script puts on the group to narrow the number: the size sits on the groups, and the texts inherit it.
  • The strike lasted 50 milliseconds and the relief rising could not be seen: now the figure arrives in the plastic’s colour and the foil runs into it afterwards, so you see the bare figure and then the silver.
  • A gradient colour cannot be animated: the foils are solid colours (the filter’s shading does the rest), animated to the colour and then left on the CSS variable, so a colour changed in the Playground also reaches the figures already minted.
  • Deleting the space between two groups, or the slash of the date, the space came straight back and nothing happened: in beforeinput the figure before it is deleted (and, at the end of the date, the slash left behind too).
  • Desaturated graphite stays as grey as before: an expired card now also loses its light and its shine, and its date sinks.
  • The Luhn verdict only came when all the hops had settled, almost two seconds after the last figure: now it comes when the light reaches the first one.
  • In the preview the number with the typo was wiped by the next click before its error showed: now the typo comes after paying and its fix is the next step, and the loop starts again from the date.
  • Far from the lamp the letters went too dark (a point light falls off with the angle): more diffuse light in the filter and the lamp a little higher.
  • With the lamp resting above the chip, the chip went white: a weaker, tighter highlight.

Libraries

GSAP 3.15.0, Hubot Sans (font variabile) 5.3.0, Martian Mono (font variabile) 5.3.0.

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

Testing

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