GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Credit Card Input UI: 9 Patterns

Credit card input UI: One of nine spaces digits into groups of four, one recalculates a validity value live as digits fill in, and one tilts a card visual as

Auto-plays · click a tile to jump to its section · all nine in one zip

Nothing in these nine demos talks to a server, touches localStorage, or rolls a random number. Every card value on screen is synthetic — the seed 9999999999999995 passes the checksum the demos compute, and 9999999999999990 fails it — and item 08's shape labels are neutral A/B/C geometry rather than any real network identification. Each pattern is one state value flipping plus a handful of constants deciding how that flip looks on screen, which is what makes the set readable as a checklist: name the value that changed and the pattern names itself.

Anatomy of nine patterns — which value changes and when

Only two of the nine run a requestAnimationFrame loop: item 08's morph interpolates six polygon points toward a target shape whenever the leading digit changes, and item 09's tilt loop chases pointer-derived targets every frame through a lerp constant. The other seven spend their motion in one-shot CSS — item 03's 180-degree flip rides a single transition on an easing token that overshoots and settles, item 04's digit-to-symbol swap staggers through per-cell transition-delay values, item 06 shakes via a keyframe animation, and the rest toggle a class once per input event. Seven of the fields are real input elements (type="text" with inputmode="numeric"); item 04 replaces the field with fixed digit spans and a toggle button, and item 09 drops typing entirely in favor of a pointer pad. Every listener in the set answers a real user event: items 01, 02, 05, 07, and 08 listen for input on their fields, item 03 for focus and blur (plus input, to repaint the code box), item 04's toggle button and item 06's Confirm button for click, item 06's field for input as well, and item 09's pad for pointermove and pointerleave.

Code per pattern

01Digit-group value toggle — as the entered digit value reaches every fourth position, a spacing value automatically appears between groups

Every input event strips non-digits from this credit card number input field, regroups into blocks of four, and reassigns the value with single spaces — then digitsBefore() turns the old caret offset into a digit count so the reformat step can correct for inserted spaces instead of jumping to the end. Under the field, paintTrack() scales a four-segment strip to g[i].length / 4 and lights each gap marker as digits pass a group boundary.

Digit-group value toggle — as the entered digit value reaches every fourth position, a spacing value automatically appears between groups
Open in a new tab

01 · Digit-group value toggle — as the entered digit value reaches every fourth position, a spacing value automatically appears between groups — Measured area 0.912% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

digitsBeforegroupsOfpaintTrack
  // Calculation layer: regroup the digit values in blocks of four, and convert the cursor's
  // position value into a digit count to correct for the inserted spacing values — plain reassignment jumps the caret to the end.
  function digitsBefore(str, upto) { return str.slice(0, upto).replace(/\D/g, '').length; }

  function groupsOf(rawDigits) {
    var g = rawDigits.match(/.{1,4}/g) || [];
    while (g.length < 4) g.push('');
    return g;
  }

  function paintTrack(rawDigits) {
    var g = groupsOf(rawDigits);
    for (var i = 0; i < 4; i++) {
      segFills[i].style.transform = 'scaleX(' + (g[i].length / 4) + ')';
    }
    for (var j = 0; j < 3; j++) {
      var on = rawDigits.length > (j + 1) * 4;
      gaps[j].classList.toggle('ci__gap--on', on);
    }
  }

What the numbers say

Item 01 fills 0.9% of the frame cumulatively at 163.0 average intensity while moving in 15 of 23 sampled frames — both the spacing and the track repaint on each keystroke of its auto loop.

02Expiry-separator value toggle — once the month and year digit values fill in, a separator value automatically appears between them

build() clamps the month into 01 through 12 — a typed 00 becomes 01, and anything above 12 collapses down to 12 — and once two month digits land, the display value gains its slash automatically. pulse() restarts the chip animation with a forced reflow (void el.offsetWidth) so the same class can fire again on consecutive cycles.

Expiry-separator value toggle — once the month and year digit values fill in, a separator value automatically appears between them
Open in a new tab

02 · Expiry-separator value toggle — once the month and year digit values fill in, a separator value automatically appears between them — Measured area 1.644% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

buildpulsedigitsOnly
  // Calculation layer: clamp the month value into 01~12, and once the month's digit
  // values fill two places, insert the separator (slash) value automatically.
  function digitsOnly(str) { return str.replace(/\D/g, '').slice(0, 4); }

  function build(rawDigits) {
    var mm = rawDigits.slice(0, 2);
    var yy = rawDigits.slice(2, 4);
    var clamped = false;
    if (mm.length === 2) {
      var n = parseInt(mm, 10);
      if (n === 0) { mm = '01'; clamped = true; }
      else if (n > 12) { mm = '12'; clamped = true; }
    }
    var slashOn = mm.length === 2;
    return { display: mm + (slashOn ? '/' : '') + yy, mm: mm, yy: yy, slashOn: slashOn, clamped: clamped };
  }

  function pulse(el) {
    el.classList.remove('ce__chip--pulse');
    void el.offsetWidth;
    el.classList.add('ce__chip--pulse');
  }

What the numbers say

Item 02 shows 1.6% cumulative area at 105.5 average intensity across 21 of 23 sampled frames — nearly continuous, because its auto loop retypes the field character by character and every keystroke pulses the chip.

03Security-code focus value toggle — when the security-code field's focus value turns on, the view switches to a value showing the card's back

The field listens for focus and blur: focus adds the flip class so the card rotates to its back in one CSS transition on the $ease-lerp-settle token, which carries it past 180 degrees and settles it back, and blur returns the front. While the back is showing, each input event refreshes a code box that pads the typed digits out to three • characters.

Security-code focus value toggle — when the security-code field's focus value turns on, the view switches to a value showing the card's back
Open in a new tab

03 · Security-code focus value toggle — when the security-code field's focus value turns on, the view switches to a value showing the card's back — Measured area 17.101% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

setFlippedcv__input--focus$ease-lerp-settle
  // Calculation layer: as the focus value turns on and off, the card's 3D rotation-angle value rides a
  // spring-like easing curve ($ease-lerp-settle), overshooting past 180deg and settling back — inertial, not constant-speed, rotation.
  function setFlipped(on) { card.classList.toggle('cv__card--flipped', on); }

  input.addEventListener('focus', function () { root.classList.remove('is-demo'); stopAutoplay(); setFlipped(true); });
  input.addEventListener('blur', function () { setFlipped(false); });
  input.addEventListener('input', function () {
    box.textContent = (input.value.replace(/\D/g, '').slice(0, 3) || '').padEnd(3, '•');
  });

  // Auto loop: walks the focus value on and off at a seeded interval.
  var SEQ = [true, true, false, false];
  var si = 0;
  var autoTimer = null;
  function autoTick() {
    var on = SEQ[si];
    setFlipped(on);
    input.classList.toggle('cv__input--focus', on);
    si = (si + 1) % SEQ.length;
  }

What the numbers say

Item 03 reaches 17.1% cumulative area at 149.4 average intensity but moves in only 6 of 23 sampled frames — a large, fast rotation that spends most of the sample resting on one face.

04Mask value toggle — when the masking value turns on, all but the first few digit values switch to a symbol value

There is no input element here at all — sixteen fixed digit spans are built once, the first KEEP (4) stay plain, and each later cell stacks a • mask span over its digit so a single root class crossfades them. Each cell carries its index as a custom property, and the SCSS turns that into transition-delay: calc(var(--i, 0) * 45ms), so the unmask spreads one digit at a time.

Mask value toggle — when the masking value turns on, all but the first few digit values switch to a symbol value
Open in a new tab

04 · Mask value toggle — when the masking value turns on, all but the first few digit values switch to a symbol value — Measured area 0.732% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

STAGGER_MSKEEPDIGITS
  var DIGITS = '9999999999999995';
  var KEEP = 4; // how many leading digit values always stay visible
  var STAGGER_MS = 45; // calculation layer: each digit value flips this much later than the one before it

  var cells = [];
  DIGITS.split('').forEach(function (ch, i) {
    var group = document.createElement('span');
    group.className = 'cm__group';
    if (i > 0 && i % 4 === 0) group.classList.add('cm__group--gap');
    if (i < KEEP) {
      group.innerHTML = '<span class="cm__digit cm__digit--fixed">' + ch + '</span>';
    } else {
      var idx = i - KEEP;
      group.style.setProperty('--i', idx);
      group.innerHTML =
        '<span class="cm__digit">' + ch + '</span>' +
        '<span class="cm__mask">•</span>';
      cells.push(group);
    }
    row.appendChild(group);
  });

What the numbers say

Item 04 covers 0.7% cumulative area at 107.3 average intensity in 7 of 23 sampled frames — the stagger trades footprint for readability, one digit flipping at a time.

05Live validity value toggle — each time a digit value fills in, the whole number's validity value is recalculated and the display value changes

luhnSum() walks the digits from the right, doubling every other one and folding values above nine, and paint() reruns it on every keystroke — validity is length >= 13 && sum % 10 === 0, and the badge draws its checkmark by animating stroke-dashoffset on a path with pathLength="1". The label beside the field prints the raw weighted sum as it changes, which keeps the checksum visible instead of a black box.

Live validity value toggle — each time a digit value fills in, the whole number's validity value is recalculated and the display value changes
Open in a new tab

05 · Live validity value toggle — each time a digit value fills in, the whole number's validity value is recalculated and the display value changes — Measured area 0.766% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

luhnSumcl__badge--onstroke-dasharray
  // Calculation layer: a positional weighted sum (doubling every other digit from the right) — the Luhn checksum is recomputed on every input.
  function luhnSum(digits) {
    var sum = 0, dbl = false;
    for (var i = digits.length - 1; i >= 0; i--) {
      var d = digits.charCodeAt(i) - 48;
      if (dbl) { d *= 2; if (d > 9) d -= 9; }
      sum += d;
      dbl = !dbl;
    }
    return sum;
  }

  function paint(digits) {
    var sum = digits.length ? luhnSum(digits) : 0;
    var valid = digits.length >= 13 && sum % 10 === 0;
    sumLabel.textContent = 'Weighted sum value ' + sum;
    badge.classList.toggle('cl__badge--on', valid);
  }

What the numbers say

Item 05 measures 0.8% cumulative area at 144.7 average intensity across 14 of 23 sampled frames — a small area with strong per-frame change, since the sum label and badge snap with each digit.

06Error-value shake toggle — when the number's error value turns on, the field's shake value rises then settles while the input value clears

verify() fires from the Confirm button and asserts exactly one condition — the digit count equals 16; anything else writes an aria-live="polite" message, restarts the ckShake keyframe through a forced reflow, and clears the field AUTO_CLEAR_MS later. Length, not the checksum, is what this pattern claims to know.

Error-value shake toggle — when the number's error value turns on, the field's shake value rises then settles while the input value clears
Open in a new tab

06 · Error-value shake toggle — when the number's error value turns on, the field's shake value rises then settles while the input value clears — Measured area 2.292% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

AUTO_CLEAR_MSshakeverify
  var AUTO_CLEAR_MS = 700; // calculation layer: the delay before the input value clears after the shake settles

  function shake() {
    field.classList.remove('ck__field--error');
    void field.offsetWidth;
    field.classList.add('ck__field--error');
  }

  function verify() {
    var digits = input.value.replace(/\D/g, '');
    if (digits.length === 16) {
      msg.textContent = '';
      return true;
    }
    msg.textContent = 'Not a 16-digit value';
    shake();
    setTimeout(function () {
      input.value = '';
      msg.textContent = '';
    }, AUTO_CLEAR_MS);
    return false;
  }

What the numbers say

Item 06 lands 2.3% cumulative area at 127.1 average intensity in 11 of 23 sampled frames — the shake is brief, and the frames after it are still by design.

07Success-value glow toggle — when the number's success value turns on, a border glow value gradually rises and the field switches to a locked value

tryLock() runs on every input event and demands both 16 digits and luhnSum(digits) % 10 === 0 before it restarts the glow; GLOW_MS after that, the field gains its locked class and the input turns readonly. The glow itself is a three-pulse cgGlow keyframe animation on the border.

Success-value glow toggle — when the number's success value turns on, a border glow value gradually rises and the field switches to a locked value
Open in a new tab

07 · Success-value glow toggle — when the number's success value turns on, a border glow value gradually rises and the field switches to a locked value — Measured area 3.308% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

GLOW_MStryLockcg__glow--on
  var GLOW_MS = 900; // calculation layer: how long the glow value takes to settle through three pulses (decay envelope)

  function luhnSum(digits) {
    var sum = 0, dbl = false;
    for (var i = digits.length - 1; i >= 0; i--) {
      var d = digits.charCodeAt(i) - 48;
      if (dbl) { d *= 2; if (d > 9) d -= 9; }
      sum += d;
      dbl = !dbl;
    }
    return sum;
  }

  function tryLock() {
    var digits = input.value.replace(/\D/g, '');
    if (digits.length === 16 && luhnSum(digits) % 10 === 0) {
      glow.classList.remove('cg__glow--on');
      void glow.offsetWidth;
      glow.classList.add('cg__glow--on');
      setTimeout(function () {
        field.classList.add('cg__field--locked');
        input.setAttribute('readonly', 'readonly');
      }, GLOW_MS);
    }
  }

What the numbers say

Item 07 covers 3.3% cumulative area at 118.4 average intensity while moving in 20 of 23 sampled frames — the auto loop types the valid number digit by digit, so the pulse train lands late in the sample.

08Pattern-match value toggle — when the leading digit value matches a set pattern value, a shape value beside it smoothly changes

paint() reads only the leading character, maps it through PREFIX_MAP to one of three neutral shapes, and when the key changes, morphTo() eases all six polygon points to the new target with easeOut(t) over MORPH_MS. The tag beside the icon shows pattern letters, never a network name.

Pattern-match value toggle — when the leading digit value matches a set pattern value, a shape value beside it smoothly changes
Open in a new tab

08 · Pattern-match value toggle — when the leading digit value matches a set pattern value, a shape value beside it smoothly changes — Measured area 1.34% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

MORPH_MSPREFIX_MAPSHAPES
  var MORPH_MS = 420; // calculation layer: how long the shape's points interpolate when the leading pattern value changes

  // Three neutral shapes (A/B/C) — unrelated to real card logos or BIN ranges.
  var SHAPES = {
    A: [[20, 4], [33.9, 12], [33.9, 28], [20, 36], [6.1, 28], [6.1, 12]],
    B: [[29, 4.4], [33, 20], [29, 35.6], [13.5, 31.3], [2, 20], [13.5, 8.7]],
    C: [[20, 1], [26.9, 16], [36.5, 29.5], [20, 28], [3.5, 29.5], [13.1, 16]],
    N: [[20, 10], [28, 15], [28, 25], [20, 30], [12, 25], [12, 15]]
  };
  var PREFIX_MAP = { '3': 'C', '4': 'A', '5': 'B' };

  var current = SHAPES.N.map(function (p) { return p.slice(); });
  var raf = null;

  function easeOut(t) { return 1 - Math.pow(1 - t, 3); }

What the numbers say

Item 08 records 1.3% cumulative area at 120.4 average intensity in just 6 of 23 sampled frames — the morph is a 420 ms event between long static holds.

09Focus and tilt values linked — as the field's focus-position value moves, the card visual's tilt value follows along

No typing here either — the pad listens for pointermove, and setTargetFromPoint() converts the pointer into -0.5 to 0.5 coordinates scaled by MAX_DEG * 2. An always-running requestAnimationFrame loop lerps curX/curY toward those targets with LERP and writes a perspective(600px) rotateX() rotateY() transform, with a sheen translating at 1.4 times the tilt.

Focus and tilt values linked — as the field's focus-position value moves, the card visual's tilt value follows along
Open in a new tab

09 · Focus and tilt values linked — as the field's focus-position value moves, the card visual's tilt value follows along — Measured area 3.647% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

MAX_DEGLERPsetTargetFromPoint
  var MAX_DEG = 14;
  var LERP = 0.14; // calculation layer: the inertial ratio at which each frame approaches the target tilt value

  var targetX = 0, targetY = 0, curX = 0, curY = 0;
  var raf = null;

  function apply() {
    curX += (targetX - curX) * LERP;
    curY += (targetY - curY) * LERP;
    card.style.transform = 'perspective(600px) rotateX(' + curX.toFixed(2) + 'deg) rotateY(' + curY.toFixed(2) + 'deg)';
    sheen.style.transform = 'translate(' + (curY * 1.4).toFixed(1) + 'px,' + (curX * -1.4).toFixed(1) + 'px)';
    raf = requestAnimationFrame(apply);
  }
  raf = requestAnimationFrame(apply);

  function setTargetFromPoint(px, py) {
    var r = pad.getBoundingClientRect();
    var nx = (px - r.left) / r.width - 0.5;
    var ny = (py - r.top) / r.height - 0.5;
    targetY = nx * MAX_DEG * 2;
    targetX = -ny * MAX_DEG * 2;
  }

What the numbers say

Item 09 moves in all 23 of 23 sampled frames at 3.6% cumulative area — the only pattern here that never fully settles, though its 55.0 average intensity is also the gentlest of the nine.

Where values reshape a pattern — spacing rule, validity calc, tilt sensitivity

Mask value toggle's STAGGER_MS, 45 to 90 doubles the per-digit delay in item 04, so unmasking spreads across the number half as fast and each group's flip reads as its own event instead of one wave.

Focus and tilt values' LERP, 0.14 to 0.28 makes item 09's card close twice as much of the remaining distance to the pointer's position each frame — stiffer, more immediate tracking that suits a compact registration card more than a wide showcase panel.

Success-value glow's GLOW_MS, 900 to 1800 stretches item 07's three-pulse envelope to double length, letting the field breathe visibly longer before the readonly lock lands.

Beyond this one post, CSS Input Design: 9 Form Widgets You Can Paste In (post 210) works the styling axis — border, focus ring, and placeholder treatments for input fields in general, of which item 01 here is one card-shaped instance. 9 Form Validation UI Patterns — As You Type (post 254) works the feedback axis: its messages react to what typing reveals, where item 05 shows the arithmetic being displayed and item 06 limits itself to length. Checkout Page Design — 9 Cart Patterns, No Library (post 278) composes the whole screen around the cart, and a credit card checkout form ui usually borrows all three axes at once — its number field is item 01's spacing, its error text is item 06's shake, and its success state is item 07's glow. Reading how a real registration screen wires those pieces together is the natural next step after comparing the three posts side by side, and the zip below is what krbccgxc opens.

Where it breaks — the instant of paste, narrow screens, reduced motion

The instant of paste

Items 05 through 08 all set maxlength="16" on a digits-only card number input field, and the browser enforces that limit before any input event fires — paste the spaced form 9999 9999 9999 9995 (19 characters) and the trailing group is cut silently, so item 05 computes its checksum over a truncated value and the badge never lights. Item 01 is the one built for that paste: its maxlength is 19, exactly 16 digits plus three spaces, so the spaced form fits whole and the reformat step can strip and regroup it.

Narrow screens

All nine carry a @media (max-width: 340px) rule in their SCSS, and none of them switches animation paths there — item 09 shrinks its card from 220x132 to 190x114 and trims the pad height, and the others compress width and padding while the same transitions keep running.

Reduced motion

Every one of the nine checks matchMedia('(prefers-reduced-motion: reduce)') in JavaScript before starting its auto loop, and every one also ships a CSS @media (prefers-reduced-motion: reduce) block — but what the check gates differs. Items 01, 02, 05, and 08 fall back to painting one settled frame instead of animating; item 04 sets the masked state immediately; item 07 fills the valid number and locks the field with no glow; items 03 and 06 simply never start their timers. Item 09 skips only its 60 ms target-sampling timer — the requestAnimationFrame lerp itself is not gated, so a reduced-motion visitor still gets pointer-driven tilt on top of the CSS transitions being disabled.

Three variants and accessibility

Variant What changes What it doesn't touch
Mask value toggle, wide stagger (STAGGER_MS 45 to 90) Item 04's per-digit delay only KEEP, the 16 fixed digit spans, markup
Focus and tilt values, stiff tracking (LERP 0.14 to 0.28) Item 09's approach ratio only MAX_DEG, setTargetFromPoint()'s coordinate math
Success-value glow, long decay (GLOW_MS 900 to 1800) Item 07's pulse duration only luhnSum(), the readonly lock, field markup

Across the nine, announced state is scarce by design: item 04's toggle button carries aria-pressed, item 06's message region carries aria-live="polite", decorative SVG in items 01, 02, 05, 07, and 08 is aria-hidden, and items 03 and 09 ship no ARIA attributes at all. All seven real inputs use type="text" with inputmode="numeric", autocomplete="off", and spellcheck="false", and nothing in the set reaches for disabled or tabindex — the states speak through visible values, not through focus order.

FAQ

Why does item 01 set its maxlength to 19 when items 05 through 08 use 16?

Because item 01's field stores the formatted value — 16 digits plus three spacing values — while the other four strip everything that is not a digit and keep the raw sixteen, so the same spaced paste lands whole in one field and truncates in the others.

Does the Luhn checksum in items 05 and 07 check the card against a payment network?

No — luhnSum() is pure arithmetic over the typed digits, nothing in the set calls a server or a payment provider, and item 08's A/B/C shapes are neutral geometry with letter labels rather than network identification.

Is item 09's tilt animated with CSS or with JavaScript?

With JavaScript — apply() runs a requestAnimationFrame loop that lerps curX and curY toward pointer-derived targets through the LERP constant, while the stylesheet only sizes the card and never transitions its transform; keeping layout declarative and tracking imperative is the division a "credit card checkout form ui design" usually preserves.

Sources: MDN — input maxlength · MDN — Window: requestAnimationFrame() method · MDN — prefers-reduced-motion

Enter the archive password

The password is inside this article. You will find it as you read.