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

チェックボックス ui 9選 — チェック値で変わる9つの方式

チェックボックス ui: 9つのうち1つはチェック値がオンになるとチェックマークの線の長さの値が0から最後まで描かれ、1つは同じ値が変わった瞬間にボックスの大きさの値が一瞬大きくなってから跳ねるように収まり、もう1つは親ボックスの値が変わると全ての子ボックスのチェック値がすぐに一緒に変わります。

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

9つのどれもサーバーに問い合わせず、localStorage に書き込まず、スクリプトのどこでも Math.random を呼びません — それぞれは、チェック値・不確定値・選択数のいずれか一つの状態値と、その値が変わったときに画面がどう見えるかを決める短い定数リストだけで組まれた、小さく自己完結したチェックボックスです。この範囲の狭さのおかげで、実際にどの値が動くのか——チェックの反転か、部分選択の比率か、それとも件数か——を名指しするだけで、コードを開く前にどの9パターンが目的のフォームに合うかが分かります。

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

9つのうち8つが自動再生ループのために少なくとも一度は requestAnimationFrame を呼びますが、実際にそのループの中で毎フレーム値を計算し直しているのは7つ——01・03・04・06・07・08・09——だけです。01の currentOffset() は、いま実際にある位置から0か getTotalLength() で測った線の全長までストローク・ダッシュオフセットを補間し、03の popScale() は Math.exp ベースの SPRING_ZETA 減衰に POP_FREQ のコサインを掛けて、SCALE_OVERSHOOT の分だけ必ず1を超えて跳ねてから収まる値を作ります。04の currentPos() は数字帯を -count * ROW_H へ向けて補間し、06のループは実測したカードの対角線 diag に対して easeOutCubic(t) * diag から clip-path: circle() の半径を求め、07の shakeOffset() は SHAKE_MS の間に振動が正確に5回になるよう周波数を合わせた減衰サイン波を計算し、08は固定ピボットを中心にシャックルの回転を UNLOCK_ANGLE へ補間し、09は一般パネルで同じ描画計算を走らせつつ削減パネルは目標へ直接飛びます。02は requestAnimationFrame を一度も呼びません——自動再生は素の setInterval で動き、計算される値は背景アルファ一つ、MIX_ALPHA + (1 - MIX_ALPHA) * ratio だけで、値が変わるたびに一度セットされたあとはブラウザ自身の CSS トランジションに任されます。05も requestAnimationFrame を呼びますが、それは自動再生の時計を進めるためだけで、階段状の見え方自体は各子ボックスに一度だけ設定される index * STAGGER_MS の transition-delay から生まれます——毎フレームのループではなくネイティブの CSS トランジションが、いつ各子ボックスが見えるようになるかを決めています。9つのどれも Math.random を呼ばず、チェックボックス アニメーション css を探して自分のコードを書く前に確かめたい人にとっても、ここにある9本のスクリプトはどれも省略のない全文のままです。リスナーはどれも本物の操作に答えます——01・02・03・04・05・09の6つは自分の <input type="checkbox"> に対する素の change だけを聞き、06は同じ change にカードを再測定する resize を組み合わせ、07は change と送信ボタンの click を組み合わせ、08だけが例外で、チェックボックス自体ではなく別のロック切替に対する click だけで動きます。prefers-reduced-motion: reduce の下では、01・03・04・06・07・08の6つが同じ計算をそのまま保ちながら持続時間の定数を 1 に落として中間フレームだけを飛ばし、02と05はもともと動きが CSS 側だったため scss で transition: none を設定するだけで、09は一般パネルと削減パネルを同じチェック値の下で並べて見せるためだけに存在します。

パターン別コード

01チェック線描画切替 — チェック値がオンになるとチェックマークの線の長さの値が0から最後まで順に描かれる方式

チェック値がオンになった瞬間、チェックマークの線の長さの値が0から最後まで描かれ、オフになると逆順に消えます。利用規約同意の項目やタスクリストの完了表示に向いた方式で、getTotalLength() がチェックマークの実際の経路長を測るため、アイコンのサイズが変わってもストロークは常にちょうど一本分で描き切れます。

チェック線描画切替 — チェック値がオンになるとチェックマークの線の長さの値が0から最後まで順に描かれる方式
新しいタブで開く

01 · チェック線描画切替 — チェック値がオンになるとチェックマークの線の長さの値が0から最後まで順に描かれる方式 — 計測 累積面積 0.442% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DRAW_MSstroke-dashoffsetgetTotalLength
  // 計算層: getTotalLength() で SVG パスの実際の長さを測り stroke-dasharray を作る — 当て推量の px は使わない
  var LEN = path.getTotalLength();
  path.style.strokeDasharray = String(LEN);

  var isDemo = true;
  var drawing = false, drawStart = 0, drawFrom = LEN, drawTo = LEN;
  path.style.setProperty('stroke-dashoffset', String(LEN));

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

  function currentOffset(ts) {
    if (!drawing) return drawTo;
    var t = Math.min(1, (ts - drawStart) / DRAW_MS);
    return drawFrom + (drawTo - drawFrom) * easeOutCubic(t);
  }

  function setChecked(next, ts) {
    input.checked = next;
    box.classList.toggle('cbd__box--on', next);
    drawFrom = currentOffset(ts);
    drawTo = next ? 0 : LEN;
    drawStart = ts;
    drawing = true;
  }

数値が語ること

01番は23フレーム中14フレームで動きながら、累積面積0.44%・平均強度73.0を記録します。currentOffset() がストロークをゼロからではなく現在実際にある位置から再開するため——drawFrom = currentOffset(ts)——描画途中で切り替えても線は途切れず連続したまま繋がります。

02不確定ダッシュ切替 — 下位項目が一部だけ選択されると上位ボックスの表示値がダッシュ形状に変わる方式

下位の選択件数の値が全部と0の間にある間、上位ボックスの表示値はチェックでも空でもなくダッシュになります。グループ選択があるファイルリストや下位オプションを持つ設定項目に向いた方式で、paintParent() が選択件数からネイティブの indeterminate プロパティを直接設定するため、CSS クラスの切り替えだけに頼らず、スクリーンリーダーにも部分選択状態が正しく伝わります。

不確定ダッシュ切替 — 下位項目が一部だけ選択されると上位ボックスの表示値がダッシュ形状に変わる方式
新しいタブで開く

02 · 不確定ダッシュ切替 — 下位項目が一部だけ選択されると上位ボックスの表示値がダッシュ形状に変わる方式 — 計測 累積面積 1.703% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

indeterminateMIX_ALPHAbackground
  function paintChildren() {
    children.forEach(function (c, i) { childBoxes[i].classList.toggle('cbi__box--on', c.checked); });
  }

  function paintParent() {
    paintChildren();
    var checkedCount = children.filter(function (c) { return c.checked; }).length;
    var ratio = checkedCount / children.length; // 計算層: 選択比率の値
    var all = ratio === 1, none = ratio === 0;
    parent.checked = all;
    parent.indeterminate = !all && !none;
    parentBox.classList.toggle('cbi__box--on', all);
    parentBox.classList.toggle('cbi__box--mixed', !all && !none);
    // 計算層: 選択比率を背景アルファへそのまま射影する(MIX_ALPHA を底にして残りの幅を ratio で埋める)
    var alpha = none ? 0 : (MIX_ALPHA + (1 - MIX_ALPHA) * ratio);
    parentBox.style.setProperty('--cbi-alpha', alpha.toFixed(3));
  }

数値が語ること

02番は23フレーム中15フレームで動き、累積面積1.70%・平均強度48.4を記録します。塗りの色そのものが計算値——MIX_ALPHA + (1 - MIX_ALPHA) * ratio——なので、上位ボックスの背景アルファは二色の切り替えではなく、実際の選択比率をそのままなぞります。

03スプリング弾み切替 — チェック値が変わった瞬間ボックスの大きさの値が目標より大きく飛び出し弾むように収まる方式

チェック値が変わった瞬間、ボックスの大きさの値が目標をわずかに超えて大きくなり、2〜3回弾んで元の大きさに収まります。チェック反応をはっきり見せたい選択項目やゲーム化されたチェックリストに向いた方式で、popScale() が減衰コサインに SCALE_OVERSHOOT を掛けるため、ボックスは毎回同じ比率でスケール1を超えて弾んでから戻ります。

スプリング弾み切替 — チェック値が変わった瞬間ボックスの大きさの値が目標より大きく飛び出し弾むように収まる方式
新しいタブで開く

03 · スプリング弾み切替 — チェック値が変わった瞬間ボックスの大きさの値が目標より大きく飛び出し弾むように収まる方式 — 計測 累積面積 0.544% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPRING_ZETASCALE_OVERSHOOTtransform
  function popScale(t) {
    // 計算層: 減衰コサイン振動 — t=0 で SCALE_OVERSHOOT だけ跳ね上がり、その後1へ収まる
    if (t < 0 || t > POP_MS) return 1;
    var amp = Math.exp(-SPRING_ZETA * (t / POP_MS));
    return 1 + amp * SCALE_OVERSHOOT * Math.cos(POP_FREQ * t);
  }

  function setChecked(next, ts) {
    input.checked = next;
    box.classList.toggle('cbs__box--on', next);
    popStart = ts;
  }

数値が語ること

03番は23フレーム中11フレームで動き、累積面積0.54%・平均強度82.7を記録します。popStart はどのトグルでも前の弾みの進み具合を無視して現在時刻へ無条件にリセットされるため、速く連続でクリックすると全振幅から弾みが再スタートし、目に見えるスナップが起きます。

04選択件数ロール切替 — 選択件数の値が上下するたびバッジ数字の桁位置の値が上下に転がるように変わる方式

選択件数の値が1つ増減するたび、バッジ数字の縦位置の値が次の数字へ転がるように変わります。複数項目を選んでまとめるリストや選択数をリアルタイムで示すフィルターに向いた方式で、目標オフセットは単純に -count * ROW_H なので、数字帯の位置は補間の当て推量ではなく行の高さの直接の倍数になります。

選択件数ロール切替 — 選択件数の値が上下するたびバッジ数字の桁位置の値が上下に転がるように変わる方式
新しいタブで開く

04 · 選択件数ロール切替 — 選択件数の値が上下するたびバッジ数字の桁位置の値が上下に転がるように変わる方式 — 計測 累積面積 0.765% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROLL_MStranslateYclip-path
  function currentPos(ts) {
    if (!rolling) return to;
    var t = Math.min(1, (ts - rollStart) / ROLL_MS);
    return from + (to - from) * easeOutCubic(t);
  }

  function paint(ts) {
    inputs.forEach(function (inp, i) { boxes[i].classList.toggle('cbc__box--on', inp.checked); });
    var count = inputs.filter(function (i) { return i.checked; }).length;
    var target = -count * ROW_H;
    if (target !== to) {
      from = currentPos(ts);
      to = target;
      rollStart = ts;
      rolling = true;
    }
  }

数値が語ること

04番は23フレーム中13フレームで動き、累積面積0.77%・平均強度83.4を記録します。currentPos() も現在実際にある位置から easeOutCubic の補間を再開するため、短い間隔で2つ連続してチェックしても、飛ばした数字の上をなめらかに転がっていきます。

05全選択カスケード切替 — 上位ボックスの全選択値がオンになると下位ボックスのチェック値が順に短い間隔で変わる方式

上位の全選択値がオンになった瞬間、下位ボックスのチェック値が上から短い間隔を置いて順に変わります。全選択ボタンがある表や一括処理を控えた項目リストに向いた方式で、各子ボックスに index * STAGGER_MS の transition-delay が一度だけ設定されるため、毎フレームのループではなくブラウザ自身の CSS トランジションが階段状のタイミングを描きます。

全選択カスケード切替 — 上位ボックスの全選択値がオンになると下位ボックスのチェック値が順に短い間隔で変わる方式
新しいタブで開く

05 · 全選択カスケード切替 — 上位ボックスの全選択値がオンになると下位ボックスのチェック値が順に短い間隔で変わる方式 — 計測 累積面積 2.429% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STAGGER_MSindeterminatetransition-delay
  // 計算層: 各子ボックスの transition-delay を index × STAGGER_MS として作る — 値そのものは
  // 一斉に変わるが、画面に見える時点だけが階段状の順序を持つ
  childBoxes.forEach(function (b, i) {
    b.style.setProperty('transition-delay', (i * STAGGER_MS) + 'ms');
  });

  function paintParent() {
    var count = childInputs.filter(function (c) { return c.checked; }).length;
    var all = count === childInputs.length, none = count === 0;
    parent.checked = all;
    parent.indeterminate = !all && !none;
    parentBox.classList.toggle('cba__box--on', all);
    parentBox.classList.toggle('cba__box--mixed', !all && !none);
  }

  function paintChildren() {
    childInputs.forEach(function (c, i) { childBoxes[i].classList.toggle('cba__box--on', c.checked); });
  }

数値が語ること

05番は23フレーム中20フレームで動き、累積面積2.43%・平均強度50.6を記録します。4つの子のチェック値は DOM 上で同じ瞬間に一斉に変わる一方、transition-delay は各子がいつ見えるようになるかだけを変えるので、値そのものとその見える時点は別のものとして扱われています。

06塗りワイプ切替 — チェック値がオンになるとボックスの背景塗りの値が一方の角から反対側へ広がるように変わる方式

チェック値がオンになった瞬間、ボックスの塗りの値が一方の角から始まり反対側の角まで広がります。アクセントカラーを強調したい選択項目やカード型の選択リストに向いた方式で、measure() がカードの実測 getBoundingClientRect() 対角線を読むため、clip-path: circle() の半径はカードのサイズが変わってもちょうど反対側の角まで届きます。

塗りワイプ切替 — チェック値がオンになるとボックスの背景塗りの値が一方の角から反対側へ広がるように変わる方式
新しいタブで開く

06 · 塗りワイプ切替 — チェック値がオンになるとボックスの背景塗りの値が一方の角から反対側へ広がるように変わる方式 — 計測 累積面積 7.924% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

WIPE_MSclip-pathbackground
  function measure() {
    // 計算層: カードの実測対角線 — 塗りの円が反対側の角にちょうど届く半径の値
    var r = card.getBoundingClientRect();
    diag = Math.sqrt(r.width * r.width + r.height * r.height);
  }

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

  function human() {
    if (isDemo) { root.classList.remove('is-demo'); isDemo = false; }
  }

  function setChecked(next, ts) {
    input.checked = next;
    wipeDir = next ? 1 : -1;
    wipeStart = ts;
    wiping = true;
  }

数値が語ること

06番は23フレーム中8フレームでしか動きませんが——9項目中でも動いたフレームが少ない部類でありながら——累積面積7.92%・平均強度199.6という9項目中もっとも強い変化を記録します。動いたフレームそれぞれで小さなボックスを揺らすのではなく、大きく塗られた円をまるごと描き直しているためです。

07必須エラー振動切替 — 必須ボックスが空のまま送信を試みるとボックスの横位置の値が短く左右に揺れる方式

送信試行の値が入った瞬間、空の必須ボックスの横位置の値が短く左右に揺れ、枠線の色の値が赤に変わります。必須同意がある登録フォームや送信前に確認が必要なチェック項目に向いた方式で、shakeOffset() は SHAKE_MS の間にちょうど5回振動するよう周波数を合わせた減衰サイン波を計算するため、警告は必ず揺れとして読め、一度きりの衝撃には見えません。

必須エラー振動切替 — 必須ボックスが空のまま送信を試みるとボックスの横位置の値が短く左右に揺れる方式
新しいタブで開く

07 · 必須エラー振動切替 — 必須ボックスが空のまま送信を試みるとボックスの横位置の値が短く左右に揺れる方式 — 計測 累積面積 9.7% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SHAKE_MStranslateXborder-color
  function shakeOffset(t) {
    // 計算層: 減衰サイン振動 — 時間が経つほど振幅が小さくなる揺れの値を直接計算する
    var amp = Math.exp(-SHAKE_ZETA * (t / SHAKE_MS));
    return SHAKE_AMP * amp * Math.sin(SHAKE_FREQ * t);
  }

  function triggerError() {
    row.classList.add('cbe__row--error');
    hint.classList.add('cbe__hint--error');
    shakeStart = performance.now();
    shaking = true;
  }

  function clearError() {
    row.classList.remove('cbe__row--error');
    hint.classList.remove('cbe__hint--error');
  }

数値が語ること

07番は23フレーム中16フレームで動き、累積面積9.70%・平均強度60.3を記録します。揺れを開始する submit クリックハンドラ自体が、ボックスのチェック値が true になった瞬間に clearError() へ切り替えるため、警告が直った後まで残ることはありません。

08無効ディム固定切替 — 無効値がオンになるとボックスの明るさの値が下がりチェック値が変化しなくなる方式

無効値がオンになった瞬間、ボックスの明るさの値が下がり、その後はクリックが来てもチェック値は変わりません。条件がまだ満たされていないオプションや他の項目に従属する選択肢に向いた方式で、チェックボックス自体ではなく別のロック切替が input.disabled を動かすため、ロックされている間はチェックボックス自身の change イベントが呼ばれることはありません。

無効ディム固定切替 — 無効値がオンになるとボックスの明るさの値が下がりチェック値が変化しなくなる方式
新しいタブで開く

08 · 無効ディム固定切替 — 無効値がオンになるとボックスの明るさの値が下がりチェック値が変化しなくなる方式 — 計測 累積面積 1.173% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

disabledfilterpointer-events
  function currentAngle(ts) {
    if (!animating) return angleTo;
    var t = Math.min(1, (ts - animStart) / LOCK_MS);
    return angleFrom + (angleTo - angleFrom) * easeOutCubic(t);
  }

  function apply(nextLocked, ts) {
    locked = nextLocked;
    input.disabled = locked;
    row.classList.toggle('cbl__row--locked', locked);
    toggle.setAttribute('aria-pressed', String(locked));
    toggleText.textContent = locked ? 'ロック解除' : 'ロック';
    angleFrom = currentAngle(ts);
    angleTo = locked ? 0 : UNLOCK_ANGLE;
    animStart = ts;
    animating = true;
  }

数値が語ること

08番は23フレーム中7フレームでしか動きません——9項目中もっとも動いたフレームが少ない——累積面積1.17%・平均強度44.1を記録します。動いているのはシャックルの回転だけで、固定された (8, 11) の点を中心に回るため、ロック本体自体は一度も再描画されません。

09モーション削減対応比較切替 — 同じチェック値の変化を一般パネルは線の長さの値を描いて、削減パネルは中間段階なしにそのまま見せる方式

同じチェック値が変わるとき、一般パネルは線の長さの値が描かれるように変わり、削減パネルは中間段階なしに目標値へ直接変わります。アクセシビリティ設定をオンにしたユーザーの画面やアニメーションを減らしたいフォームに向いた方式で、両方のパネルが同じ DURATION_MS の計算を走らせつつ、削減パネル側の change ハンドラだけが補間せずに checked ? 0 : LEN へ直接飛びます。

モーション削減対応比較切替 — 同じチェック値の変化を一般パネルは線の長さの値を描いて、削減パネルは中間段階なしにそのまま見せる方式
新しいタブで開く

09 · モーション削減対応比較切替 — 同じチェック値の変化を一般パネルは線の長さの値を描いて、削減パネルは中間段階なしにそのまま見せる方式 — 計測 累積面積 0.617% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DURATION_MSCYCLE_MSmatchMedia
  function currentOffset(ts) {
    if (!drawing) return drawTo;
    var t = Math.min(1, (ts - drawStart) / DURATION_MS);
    return drawFrom + (drawTo - drawFrom) * easeOutCubic(t);
  }

  function setState(next, ts) {
    // 一般パネル: DURATION_MS の間、実測した長さを基準に draw-on を補間する(matchMedia が false のときの実際の経路)
    normalInput.checked = next;
    normalBox.classList.toggle('cbr__box--on', next);
    drawFrom = currentOffset(ts);
    drawTo = next ? 0 : LEN;
    drawStart = ts;
    drawing = true;

    // 削減パネル: matchMedia の結果と同じ分岐の論理で、中間フレームなしに目標状態へ即座に飛ぶ
    reducedInput.checked = next;
    reducedBox.classList.toggle('cbr__box--on', next);
  }

数値が語ること

09番は23フレーム中11フレームで動き、累積面積0.62%・平均強度96.7を記録します——シーンとしてではなく、比較対象のペアとして読む数値です。二つのパネルはフレームを埋めるためではなく、並べて見比べられるために存在しています。

値が形と色を変える基準 — チェック値・不確定値・件数値

チェックボックス uiデザインと言うと単なる色替えを想像しがちですが、9つのうち3つはそれよりもう一歩先まで踏み込んでいます。06番の clip-path: circle() は色を測定した距離へ直接結びつけます。塗りの半径は easeOutCubic(t) * diag で、diag はカード自身の getBoundingClientRect() 対角線から来るため、カードの幅が変わってもチェックの色は必ず正確に反対側の角まで届いて止まります。02番の背景アルファはここでもっとも分かりやすい「比率から色」の例です。MIX_ALPHA + (1 - MIX_ALPHA) * ratio は選択件数がゼロでも22%の不透明度を底として保ち、比率が1に近づくほど残りの幅を埋めていくので、上位ボックスの濃さは常にグループの実際の選択割合と一致します。04番の数字帯は逆に、色ではなく形と位置が変わる例です。件数の値は -count * ROW_H へそのまま写されるだけで、バッジはどの数字を表示すべきか教えられているわけではなく、どこに座るべきかだけを教えられており、行の高さがどの数字が窓から見えるかを決めています。

チェック値・不確定値・件数値というこの区別は、この方式がこのページを離れて実際の設定画面で使われるときにこそ意味を持ちます。CSS トグルスイッチ 9選 — 一行でコピペOK はチェック・不確定・件数の三択ではなく一本のトラック上のハンドル位置だけを計算します——スイッチには置く場所が二つしかないからです。css トグルスイッチ 9選 — 実物みたいなスイッチをコピペで も同じハンドル位置の軸を共有しつつ、物理的に押し込まれたように見せる影とハイライトの値を重ねますが、三つ目の状態は持ちません。フォーム バリデーション UI 9選 — 打ち込んだ瞬間に判定するエラー表示 はここの07番にもっとも近い位置にありますが、その表示軸は送信時のチェック値ではなくキー入力のたびに判定される妥当性です。zip を一度開けて ukycae87 を取り出せば、9つのパターンすべてが同時に解錠されるので、項目ごとに開け直す必要はありません。

どこで壊れるか — 連続トグル、必須項目、モーション削減

それぞれ自前のタイミングを計算する9つのパターンは、実際のユーザーが一つずつ丁寧にクリックしなくなった瞬間、9つそれぞれ違う形で壊れます。

弾みや描画が収まる前の連続トグル

03番の popStart はどの change イベントでも前の弾みがどこまで進んでいたかを無視して現在時刻へ無条件にリセットされるため、十分速くダブルクリックすると全振幅から弾みが再スタートし、なめらかな継続ではなく目に見えるスナップが起きます。01番と04番はまさにこの点を避けていて、新しい目標を設定する前に currentOffset() / currentPos() を読むため、同じように速く連続トグルしても線や数字帯はゼロへ戻らず、いま実際にある位置から続きます。

必須かつ無効な項目

07番の送信ハンドラは input.checked を直接見るだけで、ブラウザ自身の reportValidity() を一度も呼ばないため、この揺れを実際のフォームへ組み込むにはネイティブの検証バブルを一緒に出すためにその呼び出しを別途加える必要があります。07番の必須項目と08番のロックを組み合わせる場合も注意が必要です——無効化されたチェックボックスはネイティブの制約検証から完全に外れるため、必須かつロック済みの項目が、本来止めるべきフォームをそのまま通してしまう可能性があります。

モーション削減、二つの異なる方法

9つのうち01・03・04・06・07・08の6つは prefers-reduced-motion: reduce の下でも同じ計算をそのまま保ち、持続時間の定数を 1 に設定するだけで、計算自体は走りながら中間フレームだけを飛ばします。02番と05番は逆の道を取ります——もともとの動きが JS のループではなくブラウザ自身の CSS トランジションだったため、scss で transition: none を設定するだけで同じ効果が得られます。09番は同じチェック値のもとで両方のアプローチを一つのデモの中に並べるためだけに存在し、一方は描き、もう一方は飛びます。

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

03番のオーバーシュート値を0.34→0.55に上げると、ボックスが目標サイズをずっと大きく超えて弾んでから収まり、反応がより遊び心のあるものになります。07番の振動の振幅を8→3pxに減らすと、空の必須ボックスの警告がずっと控えめになり、画面が騒がしくなくなります。06番の塗りの時間を420→900msに延ばすと、角から広がっていく過程がはっきり見えるようになり、チェックの瞬間の重みが増します。

バリエーション 変わるもの 触れないもの
スプリングのオーバーシュート率 SCALE_OVERSHOOT 0.34 → 0.55 popScale() の式・POP_MS・チェック値そのもの
必須エラー振動の振幅 SHAKE_AMP 8 → 3 SHAKE_FREQ・SHAKE_ZETA・送信ハンドラ
塗りワイプの時間 WIPE_MS 420 → 900 clip-path の形状・実測した対角線・チェック値

9つ全てが、クリックハンドラ付きの <div> ではなく本物の <input type="checkbox"> を装飾用ボックスの下に保っているため、余分なコードなしにスペースキーとタブキーがそのまま動き、ブラウザ自身が設定するので aria-checked も自動的に一致します。02番と05番はさらに一歩進んで、ネイティブの indeterminate と checked プロパティをスクリプトから直接設定するため、スクリーンリーダーはカスタムの CSS クラスだけを見るのではなく、部分選択と全選択の状態を正しく読み上げます。08番のロックは本物の disabled プロパティも切り替え、これが pointer-events だけでなく、ロックされたチェックボックスをキーボードフォーカスとネイティブのフォーム送信の両方から外している理由です。

FAQ

なぜ一つのチェックボックスに9つも違うトグル方式が必要なのですか

変わっている値そのものがいつも同じではないからです。01・03・06のような項目は単純なチェックの反転に向いていて、02や05のような項目は不確定な比率に向いており、04は件数に向いているため、一つの軸のために作られた方式は別の軸に無理に当てはめると大抵見た目が合いません。

これは「チェックボックス 全選択 ui」を含む9パターンの自動生成ですか、それぞれ手書きされたものですか

それぞれ独立して手書きされた9パターンです——上のデモ一つ一つが自前の定数セットを持つ自己完結したスクリプトで、隣のコードフェンスは変数を差し替えて生成したテンプレートではなく、実際にそれを動かしているブロックそのものです。

02番のダッシュはネイティブの :indeterminate CSS セレクタから来ているのですか、それともスクリプトで描かれているのですか

両方が関わっています——paintParent() は本物の indeterminate プロパティを <input> に設定するため、支援技術や他の場所にある :indeterminate セレクタも正しくそれを見ますが、ダッシュ自体の背景アルファと形は、CSS の疑似クラスだけに任されるのではなく、スクリプト内で計算された ratio の値から描かれます。

Sources: MDN — <input type="checkbox"> · MDN — :indeterminate · MDN — prefers-reduced-motion · MDN — ARIA checkbox role

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

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