GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

カード番号 入力 UI 9選 — 桁ごとに反応が変わる9つの方式

カード番号 入力 UI:9つのうち1つは入力された数字値が4桁に達するたびその間の間隔値が自動的に生まれ、1つは桁の値が埋まるたびに番号全体の有効性値が再計算されて表示値が変わり、もう1つはポインターの位置の値が変わるとカードの絵の傾きの値がその値についてきます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

この9つのデモはどれもサーバーと通信せず、localStorage にも触れず、乱数も使いません。画面に出る番号はすべて合成値です — シード 9999999999999995 はデモが計算するチェックサムを通り、9999999999999990 は通りません。08番の図形ラベルも実在のカード会社を示すものではなく、中立なA/B/Cの幾何学図形です。どのパターンも1つの状態値が切り替わり、いくつかの定数がその切り替わり方を決めているだけなので、変わった値の名前さえ言えれば方式そのものの見分けがつきます。

9つのパターンの解剖 — どの値がいつ変わるか

requestAnimationFrame のループを回す方式は9つ中2つだけです — 08番の変形は先頭の数字が変わるたびに6つの頂点座標を目標の図形へ補間し、09番の傾きループはポインターから求めた目標値へ毎フレーム一定の比率で近づきます。残り7つは一度きりのCSSで動きます — 03番の180度反転はイージングトークンに乗った1本のtransitionが行き過ぎてから戻って収まり、04番の数字↔記号切替は各セルのtransition-delayで時間差をつけ、06番はキーフレームアニメーションで揺れ、それ以外は入力イベントのたびにクラスを一度だけ切り替えます。実物の入力欄(type="text"+inputmode="numeric")を持つのは7つで、04番は入力欄の代わりに固定の数字spanとトグルボタンに置き換え、09番はそもそもタイピングをやめてポインターパッドに置き換えています。

パターン別コード

01桁グループ値切替 — 入力された数字値が4桁ごとにその間の間隔値が自動的に生まれる方式

入力イベントのたびに数字以外を取り除いて4桁ずつ再グループ化し、値を入れ直します。その前に digitsBefore() が古いカーソル位置を桁数に変換しておくことで、挿入された間隔値の分だけカーソル位置を補正でき、単純な再代入ならカーソルが末尾へ飛んでしまう問題を避けています。欄の下では paintTrack() が4本の帯を g[i].length / 4 の比率で伸縮させ、桁がグループの境界を越えるたびに区切りマーカーを点灯させます。

桁グループ値切替 — 入力された数字値が4桁ごとにその間の間隔値が自動的に生まれる方式
新しいタブで開く

01 · 桁グループ値切替 — 入力された数字値が4桁ごとにその間の間隔値が自動的に生まれる方式 — 計測 累積面積 0.912% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

digitsBeforegroupsOfpaintTrack
  // 計算層: 数字値を4個ずつ再グループ化し、カーソル位置の値を桁数に
  // 換算して挿入された間隔値の個数ぶん補正する — そのまま入れ直すとカーソルが末尾へ飛ぶ。
  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);
    }
  }

数字が語ること

01番は平均で画面の0.9%を占め、強度163.0、実測23フレーム中15フレームで動きます — 自動ループが1桁ずつ打ち込むたびに間隔値も帯も同時に描き直されるためです。

02有効期限区切り値切替 — 月と年の桁の値が埋まるとその間の区切り値が自動的に現れる方式

build() は月の値を01から12の範囲にclampします — 00と打てば01に、13以上なら12に丸められます — 月の桁が2つ埋まった瞬間、表示値にスラッシュが自動で加わります。pulse() は強制リフロー(void el.offsetWidth)でクラスを付け直し、連続したサイクルでも同じアニメーションを毎回発火させます。

有効期限区切り値切替 — 月と年の桁の値が埋まるとその間の区切り値が自動的に現れる方式
新しいタブで開く

02 · 有効期限区切り値切替 — 月と年の桁の値が埋まるとその間の区切り値が自動的に現れる方式 — 計測 累積面積 1.644% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

buildpulsedigitsOnly
  // 計算層: 月の値の範囲を01~12にclampし、月の桁の値が2桁を埋めると
  // 区切り(スラッシュ)値を自動で挟み込む。
  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');
  }

数字が語ること

02番は累積面積1.6%・強度105.5、実測23フレーム中21フレームとほぼ動き続けます — 自動ループが1文字ずつ打ち込み直し、そのたびにチップも点滅するためです。

03セキュリティコード焦点値切替 — セキュリティコード欄の焦点値がオンになるとカードの裏面を見せる値に画面が変わる方式

欄はfocusとblurだけを聞いています — focusでフリップ用クラスが付き、$ease-lerp-settle というイージングトークンに乗った1本のCSS transitionでカードが裏面へ回転し、180度を行き過ぎてから戻って収まります。blurで表面に戻ります。裏面を見せている間は入力イベントのたびにコード欄が更新され、打ち込んだ桁を3文字の•まで埋めます。

セキュリティコード焦点値切替 — セキュリティコード欄の焦点値がオンになるとカードの裏面を見せる値に画面が変わる方式
新しいタブで開く

03 · セキュリティコード焦点値切替 — セキュリティコード欄の焦点値がオンになるとカードの裏面を見せる値に画面が変わる方式 — 計測 累積面積 17.101% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

setFlippedcv__input--focus$ease-lerp-settle
  // 計算層: 焦点値のオン/オフに応じてカードの3D回転角の値がばね状のイージング曲線
  // ($ease-lerp-settle)に乗って180degを超えてから戻って収まる — 等速回転ではなく慣性のある回転。
  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, '•');
  });

  // 自動ループ: 焦点値のオン/オフをシードの間隔で行き来する。
  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;
  }

数字が語ること

03番は累積面積17.1%・強度149.4に達しますが、実測23フレーム中6フレームでしか動きません — 大きく速い回転が起きたあとは、ほとんどの時間片面で静止しています。

04マスク値切替 — マスクの値がオンになると先頭の数桁を残し残りの桁の値が記号値に変わる方式

ここには入力欄そのものがありません — 16個の数字spanを一度だけ作り、先頭のKEEP(4)桁は常にそのまま、それ以降の各セルには•のマスクspanを重ねておき、ルートのクラス1つでクロスフェードさせます。各セルはインデックスをカスタムプロパティとして持ち、SCSS側でtransition-delay: calc(var(--i, 0) * 45ms)に変換されるため、マスク解除は1桁ずつ時間差で広がります。

マスク値切替 — マスクの値がオンになると先頭の数桁を残し残りの桁の値が記号値に変わる方式
新しいタブで開く

04 · マスク値切替 — マスクの値がオンになると先頭の数桁を残し残りの桁の値が記号値に変わる方式 — 計測 累積面積 0.732% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STAGGER_MSKEEPDIGITS
  var DIGITS = '9999999999999995';
  var KEEP = 4; // 先頭の何桁を常にそのまま見せておくか
  var STAGGER_MS = 45; // 計算層: 各桁の値がこの分だけ遅れて切り替わる

  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);
  });

数字が語ること

04番は累積面積0.7%・強度107.3、実測23フレーム中7フレームで動きます — 時間差の分だけ面積は小さくなりますが、その代わり1桁ずつの切り替わりが読み取りやすくなります。

05有効性値のリアルタイム切替 — 桁の値が埋まるたびに番号全体の有効性値が計算され表示値が変わる方式

luhnSum() は右から桁を辿り、1つおきに値を2倍にして9を超えた分を折り返し、paint() が入力のたびにそれを再計算します。有効性の条件はlength >= 13 && sum % 10 === 0で、バッジはpathLength="1"を持つパス上のstroke-dashoffsetをアニメーションさせてチェックマークを描きます。欄の横のラベルは生の加重合計値をそのまま表示し続け、計算過程を隠しません。

有効性値のリアルタイム切替 — 桁の値が埋まるたびに番号全体の有効性値が計算され表示値が変わる方式
新しいタブで開く

05 · 有効性値のリアルタイム切替 — 桁の値が埋まるたびに番号全体の有効性値が計算され表示値が変わる方式 — 計測 累積面積 0.766% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

luhnSumcl__badge--onstroke-dasharray
  // 計算層: 桁ごとの加重合計(右から1桁おきに2倍) — ルーンのチェックサムを入力のたびに再計算する。
  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 = '加重合計値 ' + sum;
    badge.classList.toggle('cl__badge--on', valid);
  }

数字が語ること

05番は累積面積0.8%・強度144.7、実測23フレーム中14フレームで動きます — 面積は小さいものの1フレームごとの変化は強く、合計値ラベルとバッジが桁ごとに切り替わるためです。

06エラー値の揺れ切替 — 番号のエラー値がオンになると欄の揺れ値が大きくなってから収まり、入力値が消える方式

verify() は確認ボタンから呼ばれ、条件はただ1つ — 桁数がちょうど16かどうかだけです。それ以外ならaria-live="polite"のメッセージを出し、強制リフローでckShakeキーフレームを付け直し、AUTO_CLEAR_MS後に欄を空にします。この方式が知っているのはチェックサムではなく桁数だけです。

エラー値の揺れ切替 — 番号のエラー値がオンになると欄の揺れ値が大きくなってから収まり、入力値が消える方式
新しいタブで開く

06 · エラー値の揺れ切替 — 番号のエラー値がオンになると欄の揺れ値が大きくなってから収まり、入力値が消える方式 — 計測 累積面積 2.292% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

AUTO_CLEAR_MSshakeverify
  var AUTO_CLEAR_MS = 700; // 計算層: 揺れが収まった後、入力値を空にするまでの遅延

  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 = '16桁の値ではありません';
    shake();
    setTimeout(function () {
      input.value = '';
      msg.textContent = '';
    }, AUTO_CLEAR_MS);
    return false;
  }

数字が語ること

06番は累積面積2.3%・強度127.1、実測23フレーム中11フレームで動きます — 揺れそのものは短く、その後の静止フレームも設計どおりです。

07完了値の輝き切替 — 番号の完了値がオンになると縁の輝きの値が徐々に上がり、欄がロックの値に変わる方式

tryLock() は入力のたびに呼ばれ、16桁であることとluhnSum(digits) % 10 === 0であることの両方を満たしたときだけ輝きを付け直します。GLOW_MSが過ぎるとロック用クラスが付き、入力欄はreadonlyになります。輝きそのものは縁を3回パルスさせるcgGlowキーフレームアニメーションです。

完了値の輝き切替 — 番号の完了値がオンになると縁の輝きの値が徐々に上がり、欄がロックの値に変わる方式
新しいタブで開く

07 · 完了値の輝き切替 — 番号の完了値がオンになると縁の輝きの値が徐々に上がり、欄がロックの値に変わる方式 — 計測 累積面積 3.308% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GLOW_MStryLockcg__glow--on
  var GLOW_MS = 900; // 計算層: 輝き値が3回のパルスで収まるまでの時間(減衰包絡線)

  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);
    }
  }

数字が語ること

07番は累積面積3.3%・強度118.4、実測23フレーム中20フレームで動きます — 自動ループが有効な番号を1桁ずつ打ち込むため、パルスの連続がサンプルの後半に集中します。

08パターン一致値切替 — 先頭の数字値が決まったパターンの値に合うと横の図形値が別の形へなめらかに変わる方式

paint()は先頭の1文字だけを見てPREFIX_MAPで3つの中立な図形のいずれかへ対応させ、キーが変わるとmorphTo()がeaseOut(t)を使ってMORPH_MSの間に6つの頂点座標すべてを新しい目標へなめらかに補間します。図形の横のタグに出るのはパターンの文字だけで、実在のカード会社名は一切表示しません。

パターン一致値切替 — 先頭の数字値が決まったパターンの値に合うと横の図形値が別の形へなめらかに変わる方式
新しいタブで開く

08 · パターン一致値切替 — 先頭の数字値が決まったパターンの値に合うと横の図形値が別の形へなめらかに変わる方式 — 計測 累積面積 1.34% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MORPH_MSPREFIX_MAPSHAPES
  var MORPH_MS = 420; // 計算層: 先頭のパターン値が変わったとき図形の頂点をこの時間かけて補間する

  // 仮想図形3種(A/B/C) — 実在のカード会社ロゴやBIN範囲とは無関係な中立ラベル。
  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); }

数字が語ること

08番は累積面積1.3%・強度120.4、実測23フレーム中わずか6フレームで動きます — 420ミリ秒の変形が長い静止の合間に挟まる構成です。

09焦点値と傾き値の連動 — 欄の焦点位置の値が動くとカードの絵の傾き値も一緒についてくる方式

ここでもタイピングはなく、パッドがpointermoveを聞くだけです。setTargetFromPoint()はポインター位置を-0.5から0.5の座標に変換し、MAX_DEG * 2倍します。常時動き続けるrequestAnimationFrameループがcurX/curYをLERPの比率で目標値へ近づけ、perspective(600px) rotateX() rotateY()のtransformを書き込み、光沢は傾きの1.4倍の速さで移動します。

焦点値と傾き値の連動 — 欄の焦点位置の値が動くとカードの絵の傾き値も一緒についてくる方式
新しいタブで開く

09 · 焦点値と傾き値の連動 — 欄の焦点位置の値が動くとカードの絵の傾き値も一緒についてくる方式 — 計測 累積面積 3.647% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MAX_DEGLERPsetTargetFromPoint
  var MAX_DEG = 14;
  var LERP = 0.14; // 計算層: 目標の傾き値へ毎フレームこの比率だけ近づく慣性補間

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

数字が語ること

09番は実測23フレーム全てで動く唯一の方式で、累積面積は3.6%です — 一度も完全に静止しませんが、強度は55.0とこの9つの中で最も穏やかです。

値がパターンを変える基準 — 間隔の基準・有効性計算・傾きの感度

マスク値切替のSTAGGER_MSを45から90へ上げると、04番のマスク解除は1桁あたりの遅延が倍になり、グループの切り替わりが波ではなく1つずつの出来事として読めるようになります。

焦点値と傾き値のLERPを0.14から0.28へ上げると、09番のカードは毎フレームでポインター位置の残り距離を2倍のペースで詰めるようになり、幅広のショーケースパネルよりも小さな登録カードに合う、より即応的な追従になります。

完了値の輝きのGLOW_MSを900から1800へ上げると、07番の3パルス包絡線が2倍の長さに伸び、readonlyのロックが掛かるまで欄がより長く呼吸しているように感じられます。

この1本を離れて見ると、入力欄 デザイン 9選 — コピペで動くフォーム部品(210番)は入力欄一般の縁取り・フォーカスリング・プレースホルダーというスタイリングの軸を扱っていて、01番はそのカード形の一例に過ぎません。フォーム バリデーション UI 9選 — 打ち込んだ瞬間に判定するエラー表示(254番)はフィードバックの軸を扱い、05番が計算過程そのものを表示するのに対し06番は桁数だけに絞るという対比が同じ軸の上にあります。決済画面 デザイン 9選 — カートから支払いまでコピペOK(278番)は画面全体をカートを軸に組み立てる特集で、実際のクレジット カード 入力 フォームはこの3つの軸を同時に借りることが多く、番号欄は01番の間隔、エラー文言は06番の揺れ、完了状態は07番の輝きに相当します。3本を並べて読んだあと、実際の登録画面がこれらをどう組み合わせているかを見るのが自然な次の一歩でを見るのが自然な次の一歩です。

どこで壊れるか — 貼り付けの瞬間・狭い画面・モーション削減

貼り付けの瞬間

05番から08番はいずれも数字専用のカード番号 入力方法としてmaxlength="16"を持ち、ブラウザはどの入力イベントよりも先にこの上限を適用します。区切り付きの9999 9999 9999 9995(19文字)を貼り付けると末尾のグループが黙って切り落とされ、05番はその欠けた値でチェックサムを計算するため、バッジは決して点灯しません。01番はまさにこの貼り付けのために作られていて、maxlengthは16桁+区切り3文字ちょうどの19なので、区切り付きの値がそのまま収まり、整形の処理が数字だけを取り出して再グループ化できます。 この9つのソース全部は上のzipに入っていて、krbccgxcを入力すれば開きます。

狭い画面

9つ全てのSCSSに@media (max-width: 340px)のルールがあり、どれもこの幅でアニメーションの経路を変えません — 09番はカードを220x132から190x114へ縮め、パッドの高さも詰めますが、他は幅とパディングを詰めるだけで同じtransitionが動き続けます。

モーション削減

9つ全てが自動ループを始める前にJavaScriptでmatchMedia('(prefers-reduced-motion: reduce)')を確認し、9つ全てがCSS側にも@media (prefers-reduced-motion: reduce)のブロックを持っています — ただし、その確認が何を止めるかは方式ごとに違います。01・02・05・08番はティックを1回だけ呼んで動かさず、アニメーションせずに落ち着いた状態へ直接移ります。04番はマスク済みの状態を即座にセットし、07番は有効な番号を入れて欄をロックしたまま輝きは出しません。03・06番はそもそもタイマーを開始しません。09番だけは60ミリ秒ごとの目標サンプリング用タイマーだけを止め、requestAnimationFrameの補間ループ自体は止めないため、モーション削減を選んだ人でもCSSのtransitionは切れたままポインターに応じた傾きだけは残ります。

3つのバリエーションとアクセシビリティ

バリエーション 変わるもの 変わらないもの
マスク値切替、広い時間差(STAGGER_MS 45→90) 04番の桁ごとの遅延だけ KEEP・16個の固定数字span・マークアップ
焦点値と傾き値、硬い追従(LERP 0.14→0.28) 09番の接近比率だけ MAX_DEG・setTargetFromPoint()の座標計算
完了値の輝き、長い減衰(GLOW_MS 900→1800) 07番のパルス持続時間だけ luhnSum()・readonlyのロック・欄のマークアップ

9つの中でアナウンスされる状態はもともと少なく設計されています — 04番のトグルボタンだけがaria-pressedを持ち、06番のメッセージ領域だけがaria-live="polite"を持ち、01・02・05・07・08番の装飾的なSVGはaria-hiddenで、03・09番にはARIA属性が一切ありません。実在する7つの入力欄はすべてtype="text"+inputmode="numeric"にautocomplete="off"・spellcheck="false"を添えていて、disabledやtabindexを使う箇所は9つの中に1つもありません — 状態は見える値そのもので語られ、フォーカス順序では語られません。

FAQ

01番のmaxlengthが19で、05番から08番が16なのはなぜですか

01番の欄は整形済みの値 — 16桁の数字値+3つの区切り値 — をそのまま保持するのに対し、他の4つは数字以外を取り除いて生の16桁だけを保つからです。同じ区切り付きの貼り付けが01番ではそのまま収まり、他では切り詰められます。

05番と07番のルーンのチェックサムは実在のカード会社を判定していますか

いいえ — luhnSum()は入力された桁の純粋な算術で、この9つのどれもサーバーや決済事業者へ通信しません。08番のA/B/Cの図形も文字ラベルだけの中立な幾何学図形で、実在のカード会社を識別するものではありません。

09番の傾きはCSSとJavaScriptのどちらでアニメーションしていますか

JavaScriptです — apply()がrequestAnimationFrameのループを回し、ポインターから求めた目標値へcurX・curYをLERPの比率で近づけていて、スタイルシート側はカードの寸法だけを決め、transformを一切transitionさせていません。レイアウトを宣言的に、動きを命令的に分けるこの構造は、クレジット カード 入力 UIの多くが保っている境界線です。

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

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。