コンテンツにスキップ

レイジーパネル

レイジーパネルは、初期ページ描画時にはコンテンツを取得しない領域 です。パネルが初めて意味を持つとき — <details> が開く、タブが アクティブになる、パネルがビューポートへスクロールインする — に 読み込まれます。交差と <details> の変種は純粋に htmx 属性だけです。 タブの変種はタブ ビヘイビアの hc:tabactivated イベントと組み合わせます。

別名: 遅延読み込み、レイジーロード。

4 つの変種すべてが api/recipes/lazy-panel/ 配下の実際のデモ エンドポイント(実アプリでは自前の URL)へリクエストします。各カードは 読み込まれた UTC 時刻を表示します — 取得が遅延された証拠で、once に より再度畳んだり再アクティブ化しても再取得されません。最後のボタンは 契約の 503 エラーブランチです: htmx 2 はデフォルトで 5xx をスワップせず (HX-Reswap 単体ではそれを上書きできません)、このページで見えるのは HX-Trigger のエラートーストです。アラート本文をスロットへスワップ したい場合は htmx.config.responseHandling を設定してください。

On intersection

Loading when visible…

On <details> open

Advanced settings

Loading…

On tab activation

Loading…

Error branch (503)

最もシンプルなトリガー: ユーザーがパネルをスクロールで視界に入れたら 読み込みます。

<section
data-hx-get="/dashboards/usage"
data-hx-trigger="intersect once"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading when visible…</p>
</section>
  • intersect once は内部で IntersectionObserver を使います。
  • パネルが 25% 見えるまで待つには intersect once threshold:0.25 を 足します。

<details> の展開時(アコーディオン)

Section titled “<details> の展開時(アコーディオン)”

ネイティブの <details> 要素の中では、toggle イベントを待ち受けます:

<details>
<summary>Advanced settings</summary>
<div
data-hx-get="/settings/advanced"
data-hx-trigger="toggle from:closest details once"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading…</p>
</div>
</details>
  • from:closest details は、祖先の <details> 要素上のイベントを 待ち受けます。
  • once により、初回展開の後にパネルが再取得されることはありません。
  • ユーザーは追加のリクエストなしで畳んだり再展開したりできます。

パネル内の明示的な更新ボタンが欲しいときは、独自の htmx トリガーを持つ 別のボタンを足してください。

hc-tabs を使います — そのビヘイビアはパネルがアクティブになるたびにパネル上で hc:tabactivated を発火するので、レイジートリガーはそのイベント + once だけです。レイジーパターンはタブコントロールではなくタブ パネルに適用します:

<div class="hc-tabs">
<div class="hc-tabs__list" role="tablist" aria-label="Reports">
<button type="button" class="hc-tabs__tab" role="tab"
id="tab-overview" aria-controls="panel-overview"
aria-selected="true" tabindex="0">Overview</button>
<button type="button" class="hc-tabs__tab" role="tab"
id="tab-revenue" aria-controls="panel-revenue"
aria-selected="false" tabindex="-1">Revenue</button>
</div>
<div class="hc-tabs__panel" role="tabpanel"
id="panel-overview" aria-labelledby="tab-overview" tabindex="0"
data-hx-get="/reports/overview"
data-hx-trigger="load"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading…</p>
</div>
<div class="hc-tabs__panel" role="tabpanel"
id="panel-revenue" aria-labelledby="tab-revenue"
tabindex="0" hidden="until-found"
data-hx-get="/reports/revenue"
data-hx-trigger="hc:tabactivated once"
data-hx-swap="innerHTML">
<p class="hc-field__message">Loading…</p>
</div>
</div>

見えているパネルは load で読み込み、隠れた各パネルは自分への最初の hc:tabactivated を待ちます。aria-selected / hidden の切り替えと キーボードナビゲーションはタブビヘイビア(installTabs()、自動 インストールのバンドルに含まれます)が担います — コンポーネント側から 見た同じパターンは タブ → htmx での利用 を参照してください。

レイジーパネルには、目に見えるローディングインジケーターがほぼ常に 有効です。ユーザーが能動的に取得をトリガーしたからです。

<section
data-hx-get="/reports/revenue"
data-hx-trigger="intersect once"
data-hx-swap="innerHTML"
data-hx-indicator="this">
<div class="hc-action">
<p class="hc-field__message">Loading…</p>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</div>
</section>

data-hx-indicator="this" の自己ターゲットにより、スワップがセトリング するまでスピナーが見え続けます。レスポンスが着地すると、プレース ホルダーのコンテンツ(スピナーを含む)は置き換えられます。

エンドポイントはパネルの内側の HTML を返します:

<!-- GET /reports/revenue -->
<dl class="hc-stack">
<div><dt>This month</dt><dd>$12,400</dd></div>
<div><dt>Last month</dt><dd>$10,900</dd></div>
</dl>

エラー状態には、アラートフラグメントを 200 で返します(htmx は デフォルトで 2xx をスワップします):

<div class="hc-alert" data-variant="error" role="alert">
<strong class="hc-alert__title">Could not load</strong>
<p class="hc-alert__body">Try again in a moment.</p>
</div>
リクエストレスポンス
GET /reports/revenue(初回の表示)200 + パネルの内側の HTML(Cache-Control は通常どおり)
失敗・プレースホルダーを保つ4xx/5xx — スワップされず、プレースホルダーが残ります
失敗・同じスロットにエラーを表示200 + アラートフラグメント(上記のとおり)。非 2xx ステータス + HX-Reswap: innerHTML でも動きますが、そのステータスへの htmx:beforeSwap 許可(または htmx.config.responseHandling)が必要です — htmx ≥ 2 はデフォルトで非 2xx をスワップしません
致命的でない警告200 + コンテンツ + HX-Trigger: {"hc:toast":{…}}
  • 取得が着地する前でもパネルが読み上げられるよう、常に意味のある プレースホルダーを描画してください。空の <section> は AT には 見えません。
  • タブパネルでは、パネルが空でも role="tabpanel" + タブへの aria-labelledby のリンクを保ってください。
  • <details> ベースのパネルでは、ネイティブの <summary> がすでに 開閉状態を公開しています。追加の ARIA は不要です。
  • 隠れたレイジーパネル内のポーリングは無駄です — 更新は可視性で ゲートし(from:closest detailsintersect など)、ユーザーが 決して見ないコンテンツの取得を避けてください。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”
  • JavaScript なしでは、パネルはプレースホルダーだけを描画します。 本当に役に立つプレースホルダー(意味のある初期値、またはフルページ 版へのリンク)を選んでください。
  • <details> ベースのパネルは特にうまくデグレードします: ネイティブの 開閉は htmx なしでも動きます。
  • ユーザーが能動的に見ているのでない限り、レイジーパネル内の every Ns は避けてください。初回読み込みの intersect once と、 別のイベント駆動の更新を組み合わせます。
  • ファーストビューをレイジーロードしない。 ページロード時に見える パネルなら、遅延された取得のラウンドトリップはただレイテンシを 足すだけです。
  • レスポンスをキャッシュする。 レイジーパネルのエンドポイントは 変化の遅いコンテンツを返すことが多く、Cache-Control: max-age=60 が仕事を節約します。