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

自動保存表示 9選 — 値が秒ごとに更新される9つの保存状態

この自動保存表示の9選では、9つのうち1つは保存中という文言が点3つと共に繰り返され、1つは最終保存時刻の値が秒単位で更新されて何分前という表示に変わり、もう1つはオフライン時の保留件数の値がオンラインに戻った瞬間に0まで減っていきます。

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

9つ全てが同じ引き金——ボタンへの本物のclickで動き始めます(07だけは選択肢ごとに2つのボタンを持ち、09のリスナーはボタンではなくパネル全体に付いています)。そしてどの値も運任せでは動きません——サイン波、指数バックオフの式、スプリングの積分、mod式のずれ、SVGのパス長のいずれかで動き、Math.random()は一度も使われません。ここにはfetch()もnavigator.onLineもlocalStorageも登場せず、保存中・オフライン待機・競合はすべてブラウザ内で再計算されるローカル変数なので、以下のどの記述も「サーバーが保存を確認する」「実際の接続を検知している」という意味では読めません。項目01だけが3つの点の行にaria-hiddenを付けている唯一の項目で、装飾でしかない点をスクリーンリーダーから隠しています。

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

3つの項目(01・05・09)は経過時間をそのままサイン波の不透明度値に変えます——01は保存中に文言と点が明滅し、05は保存済みのタイムスタンプが古くなるにつれ点がフェードし、09は同じサイン波を一方の板だけに走らせ、隣の板は一切トゥイーンなしで切り替わります。2つの項目(02・08)はパーセンテージをトゥイーンする代わりにSVGの長さを描きます——02のチェックは自身のgetTotalLength()に沿って伸び、08のリングは毎ティック固定の円周から値を差し引いてカウントダウンを刻みます。項目03と項目04はどちらも値を2つの境界の間でclampしますが、03の残り再試行数は指数バックオフの待ち時間(BACKOFF_BASE_MS×2^n)に直結する一方、04の保留件数は線形なscaleXの塗りバーだけを動かします。項目06は1つのmod式オフセットで3つの点に同じ3段階カウンターを回し、点ごとに1段ずつ遅れて表示され、項目07だけが経過時間から直接値を計算するのではなく、スプリング(SPRING_K・SPRING_C)を毎フレーム積分する唯一の項目です。これは単純な自動保存トグルボタンが一度切り替わるような話ではなく、9つの値はどれもデモが動いている間ずっと同じ計算式で再計算され続けます。

パターン別コード

01保存中文言のパルス — 保存が進む間、文言の明るさの値が上下を繰り返す方式

保存が進んでいる間、文言の明るさの値は一定周期で上下し、3つの点も同じ波形を一定のずれで追いかけます。位相がSAVING_MSを超えると全ての不透明度が固定値に切り替わり、文言も保存済みの値へ入れ替わってサイン波の計算は止まります。

保存中文言のパルス — 保存が進む間、文言の明るさの値が上下を繰り返す方式
新しいタブで開く

01 · 保存中文言のパルス — 保存が進む間、文言の明るさの値が上下を繰り返す方式 — 計測 累積面積 0.408% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PULSE_PERIOD_MSDOT_STAGGER_MSSAVING_MS
  function raf(ts) {
    if (phaseStart === null) phaseStart = ts;
    var el = ts - phaseStart;
    if (phase === 'saving') {
      if (reduce) {
        label.style.opacity = '1';
        dots.forEach(function (d) { d.style.opacity = '.6'; });
      } else {
        var t = (el % PULSE_PERIOD_MS) / PULSE_PERIOD_MS;
        var op = 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2));
        label.style.opacity = op.toFixed(3);
        dots.forEach(function (d, i) {
          var dop = 0.2 + 0.8 * Math.abs(Math.sin((el - i * DOT_STAGGER_MS) / PULSE_PERIOD_MS * Math.PI));
          d.style.opacity = dop.toFixed(3);
        });
      }
      if (el > SAVING_MS) {
        phase = 'idle';
        phaseStart = ts;
        label.textContent = '保存済み';
        label.style.opacity = '1';
        dots.forEach(function (d) { d.style.opacity = '0'; });
        root.classList.add('ssp--done');
      }

数値が示すこと

23フレームの計測で、項目01はフレーム面積の0.41%だけを動かし強度スコア39.1・変化フレーム19という、短い文言と3つの点だけに留まる薄いパルスです。

02保存完了値のひらめき — 保存が終わる瞬間に完了値が1に変わり、一定時間後に0へ戻って消える方式

保存が終わる瞬間に完了値が1に切り替わり、チェックマークが一瞬現れてから、定めた保持時間の後に0へ戻って消えます。チェックマークはアイコン差し替えではなく、自身のgetTotalLength()で測った経路長に沿ってストローク・オフセットが満尺から0まで動く描画です。

保存完了値のひらめき — 保存が終わる瞬間に完了値が1に変わり、一定時間後に0へ戻って消える方式
新しいタブで開く

02 · 保存完了値のひらめき — 保存が終わる瞬間に完了値が1に変わり、一定時間後に0へ戻って消える方式 — 計測 累積面積 0.64% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DRAW_MSHOLD_MSGAP_MS
  function draw(cb) {
    if (reduce) { check.style.strokeDashoffset = '0'; cb(); return; }
    var start = null;
    function frame(ts) {
      if (start === null) start = ts;
      var t = Math.min(1, (ts - start) / DRAW_MS);
      check.style.strokeDashoffset = (len * (1 - t)).toFixed(2);
      if (t < 1) requestAnimationFrame(frame); else cb();
    }
    requestAnimationFrame(frame);
  }

  function cycle() {
    badge.classList.add('ssc__badge--show');
    draw(function () {
      timer = setTimeout(function () {
        badge.classList.remove('ssc__badge--show');
        timer = setTimeout(function () {
          check.style.strokeDashoffset = String(len);
          timer = setTimeout(cycle, GAP_MS);
        }, reduce ? 0 : FADE_MS);
      }, HOLD_MS);
    });
  }

数値が示すこと

23フレームの計測で、項目02はフレーム面積の0.64%・強度スコア53.4・変化フレーム11という、チェックが消えた後は何も残らない短い点滅です。

03保存失敗の再試行回数 — 保存が失敗するたびに残り再試行回数の値が1つ減り、文言が揺れる方式

保存が失敗するたびに残り再試行回数の値が1つ減り、その瞬間に文言が短く左右に揺れます。再試行が0になるまでに、次の待ち時間は固定の倍増式ですでに2倍ずつ伸びており、次の試行がいつ来るかをデモが黙っているわけではありません。

保存失敗の再試行回数 — 保存が失敗するたびに残り再試行回数の値が1つ減り、文言が揺れる方式
新しいタブで開く

03 · 保存失敗の再試行回数 — 保存が失敗するたびに残り再試行回数の値が1つ減り、文言が揺れる方式 — 計測 累積面積 1.646% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MAX_RETRYSHAKE_MSBACKOFF_BASE_MS
  function fail() {
    retries -= 1;
    label.textContent = '保存失敗';
    root.classList.add('ser--fail');
    shake(function () {
      if (retries > 0) {
        countEl.textContent = '残り再試行 ' + retries;
        var wait = BACKOFF_BASE_MS * Math.pow(2, MAX_RETRY - retries - 1);
        timer = setTimeout(fail, wait);
      } else {
        countEl.textContent = '再試行を使い切りました';
        root.classList.add('ser--out');
        timer = setTimeout(reset, 900);
      }
    });
  }

数値が示すこと

23フレームの計測で、項目03はフレーム面積の1.65%・強度スコア109.5・変化フレーム10という、失敗件数の周りの数文字だけに触れる鋭く短い揺れです。

04オフライン保留件数 — 接続が切れている間に積み上がる保留件数の値が、接続が戻る瞬間に0へ減っていく方式

接続が切れている間、保留件数の値は1つずつ増え、接続が戻った瞬間にその値が順番に0まで減っていきます。同じ件数に連動する塗りバーはMath.min/Math.maxで0と上限の間に固定され、どちらの端も超えて動くことはありません。

オフライン保留件数 — 接続が切れている間に積み上がる保留件数の値が、接続が戻る瞬間に0へ減っていく方式
新しいタブで開く

04 · オフライン保留件数 — 接続が切れている間に積み上がる保留件数の値が、接続が戻る瞬間に0へ減っていく方式 — 計測 累積面積 1.761% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

MAX_QUEUEADD_MSDRAIN_MS
  function paint() {
    countEl.textContent = count + '件待機';
    fill.style.transform = 'scaleX(' + (count / MAX_QUEUE).toFixed(3) + ')';
    label.textContent = online ? 'オンライン' : 'オフライン';
    root.classList.toggle('soq--online', online);
  }

  function tick() {
    if (!online) {
      count = Math.min(MAX_QUEUE, count + 1);
      paint();
      if (count >= MAX_QUEUE) { online = true; paint(); timer = setTimeout(tick, DRAIN_MS); return; }
      timer = setTimeout(tick, ADD_MS);
    } else {
      count = Math.max(0, count - 1);
      paint();
      if (count <= 0) { timer = setTimeout(function () { online = false; paint(); timer = setTimeout(tick, ADD_MS); }, 500); return; }
      timer = setTimeout(tick, DRAIN_MS);
    }
  }

数値が示すこと

23フレームの計測で、項目04はフレーム面積の1.76%・強度スコア148.7・変化フレーム22という、小さなバーと数字でありながら溜まって流れる間はほぼ全フレームで動き続ける項目です。

05最終保存時刻の更新 — 保存が終わってから経過した時間の値が毎秒再計算され、文言に変わる方式

保存が終わった後、経過時間の値は毎秒再計算され、その値は何分前という文言へ変わり続けます。同じ経過時間に連動する鮮度の値は、タイムスタンプが古くなるほど状態ドットを固定の下限へ向けてフェードさせますが、モーション削減時はその代わりに不透明度を最大で固定します。

最終保存時刻の更新 — 保存が終わってから経過した時間の値が毎秒再計算され、文言に変わる方式
新しいタブで開く

05 · 最終保存時刻の更新 — 保存が終わってから経過した時間の値が毎秒再計算され、文言に変わる方式 — 計測 累積面積 0.822% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

DEMO_SEC_MSMIN_THRESHOLD_SECRESET_AFTER_SEC
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var savedAt = performance.now();

  // 計算層: 経過秒 = (現在 − 保存時刻) ÷ 加速ms。秒・分換算で文言を作り、鮮度(不透明度)は 1 − min(1, 経過÷しきい値) で求める
  function raf(ts) {
    var elapsedSec = Math.floor((ts - savedAt) / DEMO_SEC_MS);
    if (elapsedSec >= RESET_AFTER_SEC) { savedAt = ts; elapsedSec = 0; }
    var phrase;
    if (elapsedSec < 5) phrase = 'たった今保存';
    else if (elapsedSec < MIN_THRESHOLD_SEC) phrase = elapsedSec + '秒前に保存';
    else phrase = Math.floor(elapsedSec / 60) + '分前に保存';
    label.textContent = phrase;
    var fresh = 1 - Math.min(1, elapsedSec / RESET_AFTER_SEC);
    dot.style.opacity = reduce ? '1' : (0.35 + 0.65 * fresh).toFixed(3);
    requestAnimationFrame(raf);
  }

数値が示すこと

23フレームの計測で、項目05はフレーム面積の0.82%・強度スコア104.4・変化フレーム21という、フレーム面積自体はほとんど変わらないのにほぼ毎フレーム再計算が走る値です。

06状態ドットの色循環 — 待機・進行・完了の3段階に応じてドットの色の値が順に変わる方式

段階が待機から進行へ、進行から完了へ移るたびに、ドットの色の値が順番に変わります。3つのドットは1つの段階カウンターをmod式で共有しているため、特定の1つが大半の変化を担うのではなく、3つが1段ずつずれて入れ替わります。

状態ドットの色循環 — 待機・進行・完了の3段階に応じてドットの色の値が順に変わる方式
新しいタブで開く

06 · 状態ドットの色循環 — 待機・進行・完了の3段階に応じてドットの色の値が順に変わる方式 — 計測 累積面積 0.888% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STAGE_COUNTSTEP_MSCLASSES
  // 計算層: 段階数(待機・進行・完了)・段階が進む間隔(加速ms)、そして各段階のラベル文言とCSSクラス
  var STAGE_COUNT = 3;
  var STEP_MS = 480;
  var NAMES = ['待機', '進行中', '完了'];
  var CLASSES = ['sdc__dot--idle', 'sdc__dot--busy', 'sdc__dot--done'];

  var stageIndex = 0;
  var timer = null;

  // 計算層: i番目のドットの表示段階 = (現在の段階 − i + 段階数) mod 段階数 — 値がドットごとにずれて後を追う
  function paint() {
    label.textContent = NAMES[stageIndex];
    dots.forEach(function (d, i) {
      var s = ((stageIndex - i) % STAGE_COUNT + STAGE_COUNT) % STAGE_COUNT;
      d.className = 'sdc__dot ' + CLASSES[s];
    });
  }
  paint();

  function step() {
    stageIndex = (stageIndex + 1) % STAGE_COUNT;
    paint();
    timer = setTimeout(step, STEP_MS);
  }

数値が示すこと

23フレームの計測で、項目06はフレーム面積の0.89%・強度スコア60.4・変化フレーム18という、1つのドットが動きを独占するのではなく3つが順番に持ち回る値です。

07保存競合の案内 — 同じ項目が他所で先に保存されると案内の値が現れ、選択を待つ方式

同じ項目が他所で先に保存されたと検知すると、案内の登場値が1に変わって画面に留まり、選択が行われた後にだけ値が0に戻って消えます。そのスライドはトゥイーンではなくスプリングで、剛性と減衰の定数から速度と位置を毎フレーム積分するため、バナーはわずかに行き過ぎてから静止します。

保存競合の案内 — 同じ項目が他所で先に保存されると案内の値が現れ、選択を待つ方式
新しいタブで開く

07 · 保存競合の案内 — 同じ項目が他所で先に保存されると案内の値が現れ、選択を待つ方式 — 計測 累積面積 11.108% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPRING_KSPRING_CAUTO_RESOLVE_MS
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var pos = -60;
  var vel = 0;
  var target = -60;
  var rafId = null;
  var timer = null;

  // 計算層: バナー位置をスプリング-減衰の物理式で積分 — 速度 += (−K×変位 − C×速度)×dt、位置 += 速度×dt
  function tick(ts, last) {
    var dt = Math.min(32, ts - last) / 1000;
    var disp = pos - target;
    var acc = -SPRING_K * disp - SPRING_C * vel;
    vel += acc * dt;
    pos += vel * dt;
    banner.style.transform = 'translateY(' + pos.toFixed(2) + '%)';
    if (Math.abs(disp) > 0.3 || Math.abs(vel) > 0.5) {
      rafId = requestAnimationFrame(function (t) { tick(t, ts); });
    } else {
      pos = target;
      banner.style.transform = 'translateY(' + pos.toFixed(2) + '%)';
    }
  }

数値が示すこと

23フレームの計測で、項目07はフレーム面積の11.11%・強度スコア127.8・変化フレーム10という、9つの中で最大の面積を持つ、小さな部品ではなくバナー全体がスライドして静止する値です。

08次回自動保存カウントダウン — 次の保存までの残り時間の値が0に向かって減っていくリング方式

次の自動保存までの残り時間の値は決まった数値から始まり、固定の間隔で0に向かって減り続けます。保存アイコン風の輪郭から描いたリングは、毎ティック自身の円周から値を差し引くことで同じカウントダウンを映し、アイコン画像を差し替えているわけではありません。

次回自動保存カウントダウン — 次の保存までの残り時間の値が0に向かって減っていくリング方式
新しいタブで開く

08 · 次回自動保存カウントダウン — 次の保存までの残り時間の値が0に向かって減っていくリング方式 — 計測 累積面積 2.631% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CYCLE_SECRDEMO_TICK_MS
  function paint() {
    numEl.textContent = String(remaining);
    var offset = CIRC * (1 - remaining / CYCLE_SEC);
    prog.style.strokeDashoffset = offset.toFixed(2);
    root.classList.toggle('sac--saving', remaining <= 0);
  }

  function start() {
    remaining = CYCLE_SEC;
    root.classList.remove('sac--saving');
    paint();
    if (timerId) clearInterval(timerId);
    timerId = setInterval(function () {
      remaining -= 1;
      if (remaining <= 0) {
        remaining = 0;
        paint();
        clearInterval(timerId);
        timerId = setTimeout(start, 500);
        return;
      }
      paint();
    }, DEMO_TICK_MS);
  }

数値が示すこと

23フレームの計測で、項目08はフレーム面積の2.63%・強度スコア158.9・変化フレーム18という、一度に大きく動くのではなく一定の時計に沿って更新されるリングと数字です。

09モーション削減時の静的保存表示 — モーション削減設定でアニメーションの代わりに文言の値だけが変わる方式

モーション削減がオンだと、位置や大きさの値は動かず、文言のテキスト値だけがその場で変わります。左右に並ぶ2つの板は同じ位相の値を共有していますが、一方はサイン波に沿って不透明度がトゥイーンする一方、もう一方はJSの不透明度値を一切受け取らないため、専用のスタイルではなく値を渡さないことだけで静止して見えます。

モーション削減時の静的保存表示 — モーション削減設定でアニメーションの代わりに文言の値だけが変わる方式
新しいタブで開く

09 · モーション削減時の静的保存表示 — モーション削減設定でアニメーションの代わりに文言の値だけが変わる方式 — 計測 累積面積 2.274% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SAVING_MSIDLE_MSPULSE_PERIOD_MS
  var phase = 'saving';
  var phaseStart = null;

  // 計算層: 通常パネルは経過時間に基づくsin波でゆっくり明るさが変わり、モーション削減パネルは同じ状態値をトゥイーンなしで即座に反映する
  function raf(ts) {
    if (phaseStart === null) phaseStart = ts;
    var el = ts - phaseStart;
    if (phase === 'saving') {
      var t = (el % PULSE_PERIOD_MS) / PULSE_PERIOD_MS;
      badgeA.style.opacity = (0.5 + 0.5 * (0.5 + 0.5 * Math.sin(t * Math.PI * 2))).toFixed(3);
      if (el > SAVING_MS) {
        phase = 'idle'; phaseStart = ts;
        labelA.textContent = labelB.textContent = '保存済み';
        badgeA.style.opacity = '1';
        badgeA.classList.add('srm__badge--done'); badgeB.classList.add('srm__badge--done');
      }
    } else {
      if (el > IDLE_MS) {
        phase = 'saving'; phaseStart = ts;
        labelA.textContent = labelB.textContent = '保存中';
        badgeA.classList.remove('srm__badge--done'); badgeB.classList.remove('srm__badge--done');
      }
    }
    requestAnimationFrame(raf);
  }

数値が示すこと

23フレームの計測で、項目09はフレーム面積の2.27%・強度スコア57.7・変化フレーム16という、フレーム占有率ではなく2つの板を並べて比較するための最小限の値です。

値がパターンを変える基準 — 更新周期・文言切替タイミング・色段階の基準

パルス周期を2倍に。 項目01のPULSE_PERIOD_MSを900から1800に動かすと、保存中の文言と3つの点は2倍ゆっくり呼吸するようになります。これは項目09自身が持つPULSE_PERIOD_MS(500)とは別の定数で、この2項目は1つのパルスを共有していません。

点循環の速度を上げる。 項目06のSTEP_MSを480から200に動かすと、待機・進行・完了を渡り歩く3つの点ははるかに速く追いかけ合うようになりますが、どの点がどの色段階を示すかを決めるmod式そのものは変わりません。

カウントダウンを半分に。 項目08のCYCLE_SECを10から5に動かすと、次の自動保存に向けてリングは2倍速く回ります。これは項目01自身のSAVING_MSとは別の値で、保存そのものにかかって見える時間はここでは変わらず、カウントダウンが再始動する頻度だけが変わります。

この9つのどれも単体でサーバーとの実通信を確認してはいません。マイクロインタラクション CSS 9選 — 一行でコピペOK(018)は、1つの要素が1回の操作で状態を切り替える計算軸を扱っており、この9つのように1つの状態値が複数の局面を巡り続けるものとは異なります。トースト通知 UI 9選 — 積み上がるスタック実装(255)は、複数の値が積み上がり・押し出し・消えるリストの計算軸を扱っており、単一の保存表示より1段上のレイヤーです。ログイン 失敗 ロック 9選 — 値が計算で絞られていく9つの方式(359)は、失敗回数の値が再試行の余地をロックへ向けて絞り込む計算軸を扱っており、ここでは項目03自身の再試行カウンターに最も近い兄弟です。この9つのソースはすべて下のzipに入っており、3z36yr9yを入力すれば開きます。

どこで壊れるか — オフライン復帰の瞬間・複数タブ同時保存・モーション削減

オフライン復帰の瞬間

9つのどれもnavigator.onLineを呼ばず、本物のonlineイベントも聞いていません——項目04のonlineフラグはただの変数で、自身のボタンと、件数が0か上限に達した時にだけ切り替わります。だから実際のネットワーク切断に反応しているわけではありません。onlineがfalseの間だけ保留件数を積み上げ、クリックでtrueに変わった瞬間に減らしていくデモは、自動保存 復元の見た目をしていても、実際には計算上の算術を見せているだけで、本物のオフライン検知ではありません。

複数タブ同時保存

同じ項目を別のタブで開くと、まったく独立したコピーが動き出します——9つのどれもlocalStorageやBroadcastChannel、その他タブ間で見える経路を読み書きしないため、項目07の競合バナーや項目04の保留件数を2つのタブで同時に動かしても、互いの存在にはまったく気づきません。項目07のような本物の複数タブ競合バナーには、もう一方のタブの保存を検知するための共有チャンネルがまさに必要で、それはここでは意図的に外されている部品です。

モーション削減

6つの項目——01・02・03・05・07・09——は自分自身のスクリプトの中でwindow.matchMedia('(prefers-reduced-motion: reduce)')を読み、直接分岐しています。01と09はサイン波の不透明度をやめて固定値に切り替え、02と03は描画や揺れのトゥイーンを飛ばして終端の状態へ直行し、07はスプリングの位置を積分せずに目標値へ直接スナップします。項目04・06・08はスクリプトの中で一度もreduceを確認しません——それぞれ自身のSCSSメディアクエリにあるtransition: noneだけが唯一の譲歩で、JS側が動かす塗りバー・ドットの色切り替え・リングのオフセットは前とまったく同じように毎フレーム再計算され続け、その上に乗るCSSトランジションだけが消えます。項目09は自身の分岐の上にもう1つ挙動を重ねており、モーション削減下ではオートプレイがrequestAnimationFrameから単純なsetIntervalに切り替わり、トゥイーンを一切挟まず2つの板のテキストとクラスを同時に切り替えます。これが、2つの板を並べてクリックのたびに比較できる理由でもあります。

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

バリエーション 変わる値 触れない値
パルス周期を2倍に PULSE_PERIOD_MS 900 → 1800(項目01) 項目09自身のPULSE_PERIOD_MS(500)
点循環の速度を上げる STEP_MS 480 → 200(項目06) 段階の遅れを決めるmod式そのもの
カウントダウンを半分に CYCLE_SEC 10 → 5(項目08) 項目01自身のSAVING_MS

9つの中でaria-*属性を追加しているのは項目01だけで、<span class="ssp__dots" aria-hidden="true">が3つの純粋に装飾的な点をスクリーンリーダーから隠しています。他の項目はroleもaria-liveもtabindexも一切追加していないため、この状態変化をスクリーンリーダーに届けるには、このコードを組み込む側が自分でライブリージョンを足す必要があります。9つのどれも状態変化にバンドルされたアイコン画像を差し替えてはおらず、最も近いのは項目02自身が描くチェックのパスで、getTotalLength()から作られたもので画像ファイルではありません。ネイティブな部分としては、項目01から08までがそれぞれ本物の<button type="button">から発火し(07だけは選択肢ごとに2つ)、項目09の引き金はボタン要素ではなくパネル全体へのclickリスナーで、9つのどれもdisabled属性をどの時点でも設定していません。

FAQ

01・05・08の値はrequestAnimationFrameで更新されるのに、06だけタイマーが違うのはなぜですか

項目06はSTEP_MSごとに段階のインデックスを1つ進めるだけなので、単純なsetIntervalで十分です。01・05・08はサイン波や経過時間の文言、リングのオフセットを毎回なめらかに見せる必要があるため、描画されるフレームごとにrequestAnimationFrameを使い続けます。

項目04の保留件数の値は、実際に切れた接続を反映していますか

いいえ——onlineは項目自身のボタンでしか切り替わらないただの変数なので、保留件数が積み上がって0まで流れ落ちるのはローカルなフラグの上の算術であり、本物のオフライン検知ではありません。

組み込み済みの点やチェックの代わりに、自分のアイコンに差し替えられますか

はい——点やチェック、リングのマークアップを保存アイコン フロッピーの図柄や他のアイコンに差し替えても、その上にある計算ロジックには一切触れません。どの項目も、与えられた要素のクラス・不透明度・ストロークの値のどれかを変えているだけだからです。

出典: MDN — requestAnimationFrame · MDN — prefers-reduced-motion · MDN — SVGGeometryElement: getTotalLength() method

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

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