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

無限スクロール 実装 9種 — リスト・テーブルUI基本パターン集

無限スクロール 実装の9種のうち、1つはスピナーなしで新しい行が順に滑り込み、1つは並び替えボタンを押すと行が実際に位置を入れ替えて移動し、1つは横スクロール中に固定した最初の列にだけ影が付きます。

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

以下で紹介する9つのリスト・テーブルUIパターンは、それぞれが外部ライブラリやCDNを読み込まない単体のHTML・CSS・JSファイルにまとまっているため、zipの中からどのフォルダを開いてもindex.htmlをブラウザに直接ドラッグするだけで同じ挙動を再現できます。

デモ

上に並ぶ各サムネイルを開いて実際にパターンを動かし、スクロール・クリック・ドラッグなどそれぞれが受け付ける操作で挙動を確かめてみてください。ここに載せた数値とコードは、要約や書き起こしではなく、そのままのファイルから読み取った値そのものです。

9つのパターンの解剖 — どこで計算の一層が回るか

以下の表の数値は、上のデモと同じキャプチャ列から機械的に測定した実測値です。同一条件で23回分のフレーム間差分を連続して取っています。累積面積はフレーム全体で動いたピクセルが占めた割合の合計、平均強度はピクセルが実際に動いたフレームだけで見た変化の強さ、動いたフレーム数(23フレーム中)はその23回の比較のうち何回で違いが検出されたかを示します。

# パターン 累積面積 平均強度 動いたフレーム数(23フレーム中)
01 スピナーなしで続く無限スクロール 35.55% 44.6 23
02 初回読み込み用リストスケルトン 8.96% 42.7 12
03 更新後に変わった行だけを照らすプルトゥリフレッシュ 29.58% 43.5 12
04 最後の行が消えると現れる空リストの文言 46.35% 47.9 21
05 ツールバーなしでバッジだけが数える一括選択 1.73% 45.1 9
06 片方向にだけ引き出る基本スワイプアクション 2.77% 83.9 16
07 行が実際に位置を入れ替えて並び替わる表 2.23% 121.3 9
08 横スクロールで影を連れて歩く固定カラム 4.62% 139.7 23
09 省略記号なしで点が1つ滑るページネーション 1.67% 96.7 16

04は行がすべて消えてリスト領域の高さ自体が指数減衰で縮む骨格レベルの変化のため、9パターン中最大の累積面積(46.35%)を記録しています。09は1.67%で最小、05は1.73%でそれに次いで小さく、この2つはどちらもドット1つ分・行1つ分という狭い範囲しか動かないためです。平均強度が最も高いのは08の139.7で、9種の中で唯一の最高値です(07の121.3が僅差で続きますが、最高としては扱いません)。平均強度が最も低いのは02の42.7です。動いたフレーム数が最も多いのは01と08で、どちらも23フレーム中23、最も少ないのは05と07で、どちらも23フレーム中9です。

クラスターA — 実測した幅・座標がそのまま計算の入力になります(02・07・09)

02・07・09の3パターンは、いずれも定数ではなく画面上に実在する値を実測するところから計算を始めます。02のmeasureBarWidths()はキャンバスのmeasureTextで各行テキストの実際の幅を測り、そのピクセル値をMIN_BAR_PCT以上の割合に変換してスケルトンバーの幅を決めます。07のsortBy()は並び替え前に各行へgetBoundingClientRect().topを呼んでfirstとして保存し、並び替え後に再び実測したlastとの差分だけ逆方向へ瞬間移動させてからSORT_REORDER_MSをかけて元の位置へ戻すFLIP計算を使います。09のmeasureAndMove()は移動先ドットの実際のレンダリング座標(offsetLeft)を実測し、インジケーターをその座標へ直接動かします。3パターンとも、値を仮定せず毎回測り直す点が共通しています。

クラスターB — 連続した進行度ひとつが結果全体を補間します(01・03・04)

01・03・04の3パターンは、単一の連続した進行度を1つ受け取り、それをそのまま結果全体へ広げる点で共通しています。01はセンチネルのintersectionRatioをMIN_BATCH(2)〜MAX_BATCH(6)の間へ線形に写し、その回だけのバッチ行数を決めます。03のapplyPull()は引っ張った生の距離(raw)をMAX_PULL_PX*(1-Math.exp(-raw/DAMPING))という指数減衰の式へ通し、指を離した瞬間の表示量そのものを求めます。04のstep()は目標の高さと現在の高さの差(diff)にLERP_RATE(0.18)を掛けた分だけ毎フレーム縮め、9パターンの中で唯一requestAnimationFrameを使い続けます。

クラスターC — 範囲やしきい値が以降の動作を制限・分岐させます(05・06・08)

05・06・08の3パターンは、範囲やしきい値がその後の動作をそのまま制限または分岐させます。05のapplyRange()は選んだ開始・終了インデックスの最小・最大(lo/hi)を求め、その区間のチェックボックスだけを一度にトグルします。06のpointerupハンドラーは、離す瞬間の速度がVELOCITY_OPEN_THRESHOLD(0.5)を超えるかどうかで開閉の方向をまず決め、超えなければ現在位置がACTION_WIDTHの半分を超えたかで判定します。08のupdateShadow()はview.scrollLeftをSHADOW_RAMP_PX(40)で割った値を0〜1へclamp()し、STICKY_COL_SHADOW_ALPHA(0.35)を掛けて影のアルファを求めます。

パターン別コード

ここからはパターンごとに、実際のファイルに書かれている関数をそのまま1つずつ抜き出して載せます。要約のための書き直しや途中省略はしていません。無限スクロール 実装方法を調べると、CSSのtransitionだけで新しい行をフェードインさせる例が数多く見つかりますが、以下の9つはいずれもその下で実測した値や進行度を計算する層を少なくとも1つ動かし続けています。

01スピナーなしで続く無限スクロール

スクロールが下端に近づくと、次のまとまりの行がスピナーやスケルトンなしで、既存リストの末尾へ順に滑り込みます(349のローディングフィードバック中心の実演とは異なり、こちらは行が現れる動き自体だけを扱います)。フィードや商品一覧で次ページを自動的に読み込むときに向いています。

スピナーなしで続く無限スクロール
新しいタブで開く

01 · スピナーなしで続く無限スクロール — 計測 累積面積 35.554% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

IntersectionObserveraddRowscrollTop
  // 計算層: センチネルがビューポートと重なった比率(intersectionRatio)が大きいほど一度に多くの行を追加する —
  // 画面端にわずかに触れただけなら少しだけ、深く入ってくれば一度に大きくつなげる。
  var io = new IntersectionObserver(function (entries) {
    entries.forEach(function (entry) {
      if (entry.isIntersecting && !loading && rowSeq < MAX_ROWS) {
        loading = true;
        var batch = Math.round(MIN_BATCH + (MAX_BATCH - MIN_BATCH) * clamp(entry.intersectionRatio, 0, 1));
        for (var j = 0; j < batch; j += 1) { addRow(j * SCROLL_APPEND_STAGGER_MS); }
        window.setTimeout(function () { loading = false; }, LOAD_COOLDOWN_MS);
      }
    });
  }, { root: view, threshold: [0, 0.2, 0.4, 0.6, 0.8, 1] });
  io.observe(sentinel);

02初回読み込み用リストスケルトン

データが届く前は画面全体が行の形をしたグレーのスケルトンバーで埋まり、実データが到着した瞬間に一括で置き換わります(004の個別スケルトン部品や349のスクロール追加時スケルトンとは異なる実演で、こちらは初回のみの全体一括置換です)。初回アクセス時のリストやフィードの読み込み待ち画面に使えます。

初回読み込み用リストスケルトン
新しいタブで開く

02 · 初回読み込み用リストスケルトン — 計測 累積面積 8.956% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

measureBarWidthssetLoadedanimation-fill-mode
  // 計算層: 各行が読み込み完了後に実際に表示する文字のキャンバス実測幅を測り
  // スケルトンバーの幅をその比率で決める — 固定幅ではなく実コンテンツの長さを先取りして反映する。
  function measureBarWidths() {
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    ctx.font = '600 13px -apple-system, "Segoe UI", "Noto Sans KR", sans-serif';
    var widths = ROWS.map(function (text) { return ctx.measureText(text).width; });
    var maxW = Math.max.apply(null, widths);
    return widths.map(function (w) {
      var pct = (w / maxW) * 100;
      return Math.max(MIN_BAR_PCT, Math.round(pct));
    });
  }

03更新後に変わった行だけを照らすプルトゥリフレッシュ

引っ張って更新を終えると、リスト全体を再描画する代わりに値が変わった行だけへ短いハイライトスイープが流れ、何が更新されたかを示します(353の引っ張る動作・アイコン・弾性自体を扱う実演とは焦点が異なり、こちらは指を離した後の結果表示だけを扱います)。相場や在庫のように値だけが変わる一覧に向いています。

更新後に変わった行だけを照らすプルトゥリフレッシュ
新しいタブで開く

03 · 更新後に変わった行だけを照らすプルトゥリフレッシュ — 計測 累積面積 29.576% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyPullsweepRowcomputeChanged
  // 計算層: 生の引っ張り量(raw)を指数減衰の抵抗曲線で抑えて表示量を作る —
  // 最初は指の動きにそのまま追従し、引くほど伸びが徐々に鈍くなる。
  function applyPull(raw) {
    var displayed = MAX_PULL_PX * (1 - Math.exp(-raw / DAMPING));
    zone.style.setProperty('--pull-px', displayed.toFixed(1) + 'px');
    spinner.style.setProperty('--spin-deg', (displayed / MAX_PULL_PX * 320).toFixed(1) + 'deg');
    indicator.style.opacity = Math.min(1, displayed / (RELEASE_THRESHOLD_PX * 0.7)).toFixed(2);
    lastDisplayed = displayed;
    return displayed;
  }

04最後の行が消えると現れる空リストの文言

最後に残った行が消えた瞬間、リスト領域に小さなアイコンと案内文が一行、薄く現れます(134のダッシュボードウィジェット内の追加モーフイラストや243の9種イラストアニメーションとは異なる実演で、こちらはイラストなしでリストが自ら空になったと知らせる最も簡素な版です)。カートやタスクリストが空になったときに使えます。

最後の行が消えると現れる空リストの文言
新しいタブで開く

04 · 最後の行が消えると現れる空リストの文言 — 計測 累積面積 46.348% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

computeTargetremoveRowtransform-origin
  function step() {
    var diff = targetHeight - currentHeight;
    if (Math.abs(diff) < 0.5) {
      currentHeight = targetHeight;
      empty.style.height = currentHeight + 'px';
      empty.style.opacity = (currentHeight / EMPTY_HEIGHT).toFixed(2);
      rafId = null;
      return;
    }
    currentHeight += diff * LERP_RATE;
    empty.style.height = currentHeight + 'px';
    empty.style.opacity = Math.min(1, currentHeight / EMPTY_HEIGHT).toFixed(2);
    rafId = window.requestAnimationFrame(step);
  }

05ツールバーなしでバッジだけが数える一括選択

チェックボックスを1つ入れるたびにその行の背景だけがうっすら色づき、リスト上部ヘッダーの小さなバッジの数字が選択数に変わります(207のbulk-select-checkbox-toolbar — 画面下からツールバーがせり上がる版 — とは異なる実演で、こちらはツールバーなしでヘッダーバッジ1つだけで知らせます)。メールボックスやファイル一覧の複数選択に向いています。

ツールバーなしでバッジだけが数える一括選択
新しいタブで開く

05 · ツールバーなしでバッジだけが数える一括選択 — 計測 累積面積 1.725% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyRangeupdateBadgearia-checked
  // 計算層: 直前に扱ったインデックスと今回のインデックスの間の範囲を算術で求め
  // その区間全体を一度に同じ状態へ揃える — 1つずつ押さなくてもよい。
  function applyRange(fromIdx, toIdx, checked) {
    var lo = Math.min(fromIdx, toIdx);
    var hi = Math.max(fromIdx, toIdx);
    for (var i = lo; i <= hi; i += 1) {
      setChecked(checkboxes[i], checked);
    }
  }

06片方向にだけ引き出る基本スワイプアクション

行を横に押すと裏に隠れていたアイコンボタンが1つだけ決まった幅まで現れ、指を離すとその幅でスナップして開くか元に戻ります(207のswipe-actions-delete-revealやswipe-actions-multi-action-stack — 削除専用の色分けや複数ボタンの重ね表示を扱う版 — とは異なる実演で、こちらはボタン1つと単純な開閉スナップだけの最も基本的な形です)。メッセージやタスク一覧で1つの素早い操作に向いています。

片方向にだけ引き出る基本スワイプアクション
新しいタブで開く

06 · 片方向にだけ引き出る基本スワイプアクション — 計測 累積面積 2.771% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

openRowcloseRowcurrentX
    content.addEventListener('pointerup', function () {
      if (!dragging) { return; }
      dragging = false;
      // 計算層: 指を離す瞬間の速度がしきい値を超えれば、止まった位置に関係なく
      // 速度の向きが開閉を決める。遅ければ位置が半分を超えたかどうかで決める。
      var openDecision;
      if (velocity < -VELOCITY_OPEN_THRESHOLD) {
        openDecision = true;
      } else if (velocity > VELOCITY_OPEN_THRESHOLD) {
        openDecision = false;
      } else {
        openDecision = currentX() < -ACTION_WIDTH / 2;
      }
      if (openDecision) { openRow(); } else { closeRow(); }
    });

07行が実際に位置を入れ替えて並び替わる表

列見出しをクリックすると値が並び替わるだけでなく、各行が新しい順位の位置まで実座標を移動するアニメーションを描いて実際に入れ替わります(257の複数列同時ソートや362の行挿入・削除トランジションとは異なる実演で、こちらは単一列クリックソートにおける行位置移動そのものだけを計算します)。価格や名前順の並び替えが必要な管理画面の表に向いています。

行が実際に位置を入れ替えて並び替わる表
新しいタブで開く

07 · 行が実際に位置を入れ替えて並び替わる表 — 計測 累積面積 2.227% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

sortByupdateHeadersaria-sort

    var rows = getRows();
    var first = {};
    rows.forEach(function (row) { first[row.dataset.id] = row.getBoundingClientRect().top; });

    rows.sort(comparator(key, dir));
    rows.forEach(function (row) { tbody.appendChild(row); });

    rows.forEach(function (row) {
      var last = row.getBoundingClientRect().top;
      var delta = first[row.dataset.id] - last;
      if (delta) {
        row.style.transition = 'none';
        row.style.transform = 'translateY(' + delta + 'px)';
        void row.offsetHeight;
        row.style.transition = '';
        row.style.transform = 'translateY(0)';
      }
    });

08横スクロールで影を連れて歩く固定カラム

表を横にスクロールすると一番左の列はその場に固定されたままで、他の列がその下を通り過ぎる間、固定列の右端にだけ淡い影がスクロール量に比例して濃くなります(207のsticky-columns-header-freeze — 縦スクロールで上部見出しを固定する版 — とは異なる軸の実演で、こちらは横スクロールで左列を固定します)。列の多い表で名前列を固定したいときに向いています。

横スクロールで影を連れて歩く固定カラム
新しいタブで開く

08 · 横スクロールで影を連れて歩く固定カラム — 計測 累積面積 4.625% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

updateShadowscrollLeftposition:sticky
  // 計算層: 横スクロール量(scrollLeft)をSHADOW_RAMP_PXで正規化し、影のアルファを算術で作る —
  // スクロールがなければ影もなく、少し動いただけですぐ上限近くまで濃くなる。
  function updateShadow() {
    var raw = clamp(view.scrollLeft / SHADOW_RAMP_PX, 0, 1);
    var alpha = raw * STICKY_COL_SHADOW_ALPHA;
    view.style.setProperty('--shadow-alpha', alpha.toFixed(3));
  }
  view.addEventListener('scroll', updateShadow, { passive: true });
  updateShadow();

09省略記号なしで点が1つ滑るページネーション

前・次矢印の間に並ぶ点の後ろで、点と同じ大きさ・形の塗りつぶされたピルが現在のページを示す点の位置まで滑って吸い付きます(207のpagination-numbered-ellipsis — 数字と省略記号でページ数が多い場合を扱う版 — とは異なる実演で、こちらはページ数が少ない場合の点表示と矢印だけで完結する最も簡素な版です)。3〜7ページ程度の短い一覧の移動に向いています。

省略記号なしで点が1つ滑るページネーション
新しいタブで開く

09 · 省略記号なしで点が1つ滑るページネーション — 計測 累積面積 1.672% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

measureAndMovegoToaria-current
  function goTo(index) {
    index = clamp(index, 0, total - 1);
    if (index === current) { return; }
    current = index;
    dots.forEach(function (dot, i) {
      if (i === current) { dot.setAttribute('aria-current', 'page'); } else { dot.removeAttribute('aria-current'); }
    });
    measureAndMove(current);
    panel.textContent = PANEL_TEXTS[current];
    prevBtn.disabled = current === 0;
    nextBtn.disabled = current === total - 1;
  }

値がUIを変える場所 — 追加のスタガー・並び替え移動時間・固定列の影

多くのリスト ui デザインの解説はフェードの長さだけで終わりますが、上の各パターンは実際に働く変数を1つだけ持っており、その1つの値を変えるだけでパターン全体の印象が変わります。

01 — バッチ数を決める交差率

batchはMath.round(MIN_BATCH + (MAX_BATCH - MIN_BATCH) * clamp(entry.intersectionRatio, 0, 1))という式で求まり、センチネルがどれだけ深く画面に入ったかで2から6の間のどの整数になるかが変わります。値は毎回のスクロールで実測され、固定値ではありません。

02 — 実測幅から求めるバー幅

measureBarWidths()が返す配列は、各行テキストの実測幅を最大幅で割った比率をMIN_BAR_PCT(32)%以上に切り上げたものです。短い行ほどバーも細く、スケルトンの時点でおおよその内容量が伝わります。

03 — 指数減衰の抵抗係数

DAMPING(90)はapplyPull()の式MAX_PULL_PX*(1-Math.exp(-raw/DAMPING))の分母で、値が小さいほど同じ生の距離でも表示量が早く頭打ちになり、指がまだ動いていても画面上の当たりが重く感じられます。

04 — 目標へ近づく比率

LERP_RATE(0.18)はstep()が毎フレーム縮める割合で、1に近いほど一気に目標の高さへ達し、0に近いほどゆっくり収束します。この値だけが、9パターン中唯一requestAnimationFrameを使う04の速度を決めています。

05 — 範囲選択のインデックス

applyRange()が求めるlo/hiは固定の定数ではなく、そのときシフトクリックした2点のインデックスから毎回計算し直されるため、選ぶ範囲が変わるたびにトグルされる行の数もそのまま変わります。

06 — 開閉を分ける速度のしきい値

VELOCITY_OPEN_THRESHOLD(0.5)を離す瞬間の速度が超えれば、移動距離に関係なく方向だけで開閉が決まります。超えなければ現在位置がACTION_WIDTH(72)の半分を超えたかどうかで判定されます。

07 — 並び替え後の移動時間

SORT_REORDER_MS(250)は、実測したfirstとlastの差分だけ逆方向へ瞬間移動させたあと、元の位置へ戻すトランジションの時間です。この値を伸ばすほど、どの行がどこへ動いたかを目で追いやすくなります。

08 — 影の濃さの上限

STICKY_COL_SHADOW_ALPHA(0.35)はupdateShadow()がview.scrollLeftから求めた0〜1の比率に掛ける上限値で、SHADOW_RAMP_PX(40)を変えると同じ0.35の濃さに達するまでに必要なスクロール量そのものが変わります。

09 — 実測されるドットの座標

measureAndMove()は移動先ドットのoffsetLeftをそのつど実測してインジケーターをその座標へ動かすため、ドット間隔を固定pxと仮定していません。画面幅が変わってresizeが発生しても、同じ計算をやり直すだけでインジケーターの位置がずれません。

どこで壊れるか — アクセシビリティ・reduced-motion・キーボードフォーカス

08の固定カラムの実装では、表を横にスクロールできるコンテナ(.stc__view)にも、内部のセルにもtabindexやボタン・リンクの類が1つもありません。マウスやトラックパッド、タッチでスクロールすれば影の計算は正しく動きますが、キーボードだけで操作している人にはそのコンテナへフォーカスを移す手段自体がなく、横スクロール自体を始めることができません。表の中身をTabで巡っても止まる場所がないため、影が濃くなる場面をキーボードだけで再現する方法は、このデモの中には用意されていません。

reduced motionで失われるもの

  • 01 スピナーなしの無限スクロール — 自動スクロールと登場トゥイーンだけが止まります。IntersectionObserver自体は生きたままなので、実際にスクロールすると次のバッチはアニメーションなしで即座に追加されます。
  • 02 リストスケルトン — スケルトンの光沢や登場トゥイーンなしで、読み込み完了状態がすぐに表示されます。
  • 03 プルトゥリフレッシュ — 自動の引っ張りループとスイープアニメーションを止め、最新の値だけを表示します。
  • 04 空リスト文言 — 退場トゥイーンなしで即座に削除され、animationendを待ちません(9種の中でここだけの例外です)。自動削除ループと登場トゥイーンも止まり、行がある状態で固定表示されます。
  • 05 一括選択 — 自動選択ループとバッジのポップトゥイーンが止まりますが、クリックや範囲選択の計算自体はそのまま動きます。
  • 06 スワイプアクション — 自動スワイプループを止め、閉じた状態で固定表示します。
  • 07 並び替え表 — 自動並び替えループを止めます。ヘッダークリックでの並び替えとFLIP計算自体はそのまま動き、トランジションだけが外れます。
  • 08 固定カラム — 自動の横スクロールループを止めます。実際にスクロールすれば影の計算自体はそのまま動きます。
  • 09 ページネーション — 自動ページ送りループを止めます。クリックや矢印キーでの移動と位置計算はそのまま動きます。

9つのファイルはすべて同じパスワード、kps8kwrz、で守られた1つのzipにまとまっています。解凍すれば、ここまで見てきた9パターンの動くコピーがそのままフォルダごとに並び、ブラウザで開くだけですぐ試せます。

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

ここでのバリエーションは、外部の無限スクロール サイトのライブラリを一切必要とせず、アーカイブ内の同じファイルに含まれる定数を書き換えるだけで試せます。

バリエーション 値 感触
新しい行の登場スタガー遅延を広げる 01 SCROLL_APPEND_STAGGER_MS 40 → 90 新しく追加される行同士の間隔が40msから90msに伸び、一気にまとまって付くのではなく一行ずつはっきり着地するのが見えます。バッチ数はそのままなので行数は変わらず、登場のリズムだけが遅くなります。
並び替え時の行移動アニメーション時間を延ばす 07 SORT_REORDER_MS 250 → 500 並び替えボタンを押したときに行が旧位置から新位置まで移動する時間が250msから500msに伸び、どの行がどこへ動いたか目で追いやすくなります。その分、並び替えが確定するまでの体感待ち時間は長くなります。
固定カラムの影の濃さを下げる 08 STICKY_COL_SHADOW_ALPHA 0.35 → 0.15 横に最後までスクロールしたとき、固定列右端の影の最大濃度が0.35から0.15に下がり、下にコンテンツがスクロールしているという合図がかなり薄くなります。影が濃いときより表は平坦に見えますが、目を引きにくくなります。

9つのパターンはそれぞれ異なるARIAの作法で状態を伝えています。05は各チェックボックスにaria-checkedを持たせ、選択数が変わるたびヘッダーのaria-live="polite"領域が新しい件数を読み上げます。07は列見出しのaria-sortを並び替えのたびにnone・ascending・descendingへ書き換え、09は現在のページの点にだけaria-current="page"を付けながら各ドットに個別のaria-labelを持たせます。02は読み込み中の領域にaria-busy="true"を、03と04は更新結果や空の状態をaria-live="polite"で知らせます。一方で06と08は装飾的な視覚効果だけで完結しており、追加のARIA属性を持ちません。

FAQ

無限スクロールとは何ですか、この01は他の無限スクロールとどう違いますか。

無限スクロールとは、ページ送りの操作なしにスクロールだけで次のデータが自動的に読み込まれる仕組みを指します。ここでの01は、スクロール位置だけでなくセンチネルのintersectionRatioを実測してバッチ行数をMIN_BATCH〜MAX_BATCHの間で変え、スピナーやスケルトンを一切表示しないまま行を末尾へ滑り込ませる実装です。349の無限スクロールはローディングのフィードバック表現そのものを主役にしており、同じ「無限スクロール」という言葉でも扱う場面が異なります。

9つのリスト・テーブルUIパターンは、実際のデータベースやAPIが必要ですか。

いいえ。9つのファイルすべてのデータは配列や定数として書かれた架空の値で、fetchのようなリアルタイムの呼び出しもMath.random()も使われていません。そのため、どのデモを再読み込みしても同じ順序・同じ数値で動きます。

prefers-reduced-motionが本当に尊重されているかは、どう確認すればよいですか。

macOSやWindowsの「視差効果を減らす」「アニメーションを表示しない」といった設定をオンにしてから、上の9つを開き直してみてください。いずれのファイルもwindow.matchMedia('(prefers-reduced-motion: reduce)')の.matchesを起動時に1回だけ読み取り、それが真であれば自動再生ループとイージング部分だけを取り除きます。ただし04だけはanimationendの待機そのものを省いて即座に行を取り除くため、他の8つとは止まり方が少しだけ異なります。

出典: MDN — Intersection Observer API、MDN — Pointer events、MDN — aria-sort、MDN — position。

関連記事: スワイプ削除リストUI 9選 — ページネーションまで · 無限スクロールローディング UIパターン9選 — スピナーから仮想ウィンドウまで · 引っ張って更新 スマホ 9選 — 引いた距離で変わる更新のかたち

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

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