レイジーパネル
レイジーパネルは、初期ページ描画時にはコンテンツを取得しない領域
です。パネルが初めて意味を持つとき — <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
On <details> open
Advanced settings
On tab activation
Error branch (503)
交差時(ビューポート内)
Section titled “交差時(ビューポート内)”最もシンプルなトリガー: ユーザーがパネルをスクロールで視界に入れたら 読み込みます。
<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 トリガーを持つ 別のボタンを足してください。
タブのアクティブ化時
Section titled “タブのアクティブ化時”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 titled “インジケーター”レイジーパネルには、目に見えるローディングインジケーターがほぼ常に 有効です。ユーザーが能動的に取得をトリガーしたからです。
<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" の自己ターゲットにより、スワップがセトリング
するまでスピナーが見え続けます。レスポンスが着地すると、プレース
ホルダーのコンテンツ(スピナーを含む)は置き換えられます。
サーバレスポンス契約
Section titled “サーバレスポンス契約”エンドポイントはパネルの内側の 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 titled “アクセシビリティ”- 取得が着地する前でもパネルが読み上げられるよう、常に意味のある
プレースホルダーを描画してください。空の
<section>は AT には 見えません。 - タブパネルでは、パネルが空でも
role="tabpanel"+ タブへのaria-labelledbyのリンクを保ってください。 <details>ベースのパネルでは、ネイティブの<summary>がすでに 開閉状態を公開しています。追加の ARIA は不要です。- 隠れたレイジーパネル内のポーリングは無駄です — 更新は可視性で
ゲートし(
from:closest details、intersectなど)、ユーザーが 決して見ないコンテンツの取得を避けてください。
プログレッシブエンハンスメント
Section titled “プログレッシブエンハンスメント”- JavaScript なしでは、パネルはプレースホルダーだけを描画します。 本当に役に立つプレースホルダー(意味のある初期値、またはフルページ 版へのリンク)を選んでください。
<details>ベースのパネルは特にうまくデグレードします: ネイティブの 開閉は htmx なしでも動きます。
- ユーザーが能動的に見ているのでない限り、レイジーパネル内の
every Nsは避けてください。初回読み込みのintersect onceと、 別のイベント駆動の更新を組み合わせます。 - ファーストビューをレイジーロードしない。 ページロード時に見える パネルなら、遅延された取得のラウンドトリップはただレイテンシを 足すだけです。
- レスポンスをキャッシュする。 レイジーパネルのエンドポイントは
変化の遅いコンテンツを返すことが多く、
Cache-Control: max-age=60が仕事を節約します。
- データ領域レシピ — eager + 自動更新の兄弟。
- カードコンポーネント — レイジーパネルの定番の視覚ラッパー。