ビューポート侵入で遅延ロード
stimeo--lazy-frame
Turbo フレームの読み込みを、近づくまで待たせます。キーボードで辿り着いたときも読み込みます。
Turbo フレームの読み込みを、画面に近づくまで待たせて、最初の表示を軽くします。どのくらい手前から読み始めるかは決められるので、スクロールが速くても間に合います。キーボードでそのフレームへ辿り着いたときにも読み込むので、スクロールしない人が空のまま取り残されません。読み込みが済んだことは印として残るので、Turbo がページを覚えていても二重には読み込みません。一度きりで終わるか、離れて戻るたびに読み直すかを選べます。読み込み中の見せ方は、フレーム読込状態の部品の仕事です。
下へスクロールしてください。下のフレームは URL を持ったまま待ち、画面に入って初めて読み込みます。
キーボード操作
このコンポーネント自体はキーボード操作を持ちません。
<%# Lazy-frame demo: in production the controller goes on a <turbo-frame> and writes its
held url to src to start the Turbo load. The catalog runs Turbo, so to avoid a real
fetch this demo puts the controller on a plain <div> (writing src to a div is inert)
and demo.js swaps the placeholder on the load event — demonstrating the lazy *trigger*
(it fires only once the frame scrolls into view, thanks to the spacer above it). The
library only writes src and reflects data-lazy-loaded. %>
<div class="lazy-frame-demo">
<p class="lazy-frame-demo__hint"><%= t("components.lazy_frame.demo.hint") %></p>
<div class="lazy-frame-demo__spacer" aria-hidden="true"></div>
<div
class="lazy-frame-demo__frame"
data-controller="stimeo--lazy-frame"
data-stimeo--lazy-frame-url-value="/posts/1/comments"
data-stimeo--lazy-frame-root-margin-value="0px"
data-loaded-label="<%= t("components.lazy_frame.demo.loaded") %>"
data-loading-label="<%= t("components.lazy_frame.demo.loading") %>">
<%= t("components.lazy_frame.demo.placeholder") %>
</div>
</div>
/*
* Presentation-only styles for the lazy-frame demo. The library writes src and reflects
* data-lazy-loaded; this CSS lays out the placeholder frame, pushes it below the fold with
* a spacer so the lazy trigger is visible on scroll, and tints it once loaded.
*/
.lazy-frame-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 28rem;
}
.lazy-frame-demo__hint {
margin: 0;
color: var(--color-text-muted);
}
/* Pushes the frame below the fold so it loads on scroll, not on initial render. */
.lazy-frame-demo__spacer {
height: 60vh;
}
.lazy-frame-demo__frame {
padding: 1.5rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
text-align: center;
color: var(--color-text-muted);
}
/*
* The visible state is driven by a demo-owned `data-demo-state` (set by demo.js),
* not the library's `data-lazy-loaded`: the library marks the load *triggered* the
* instant the frame enters the viewport, but the demo simulates Turbo's fetch
* latency so the transition is perceptible — loading (amber) then loaded (green).
*/
.lazy-frame-demo__frame[data-demo-state="loading"] {
border-style: solid;
border-color: var(--amber-500);
background: var(--amber-50);
color: var(--amber-500);
}
.lazy-frame-demo__frame[data-demo-state="loaded"] {
border-style: solid;
border-color: var(--leaf-500);
background: var(--leaf-50);
color: var(--leaf-500);
}
// Lazy-frame demo (consumer-side JS).
//
// In production the lazy-frame controller writes its held url to a <turbo-frame>'s src and
// Turbo fetches it. The catalog has no backend for that, so the demo runs the controller
// on a <div> (writing src to a div does nothing) and reflects the load event into the
// placeholder — proving the lazy *trigger* fired only once the frame scrolled into view.
//
// The library marks the load *triggered* the instant the frame enters the viewport, so a
// naive swap would flip to "loaded" before it can be perceived. To mirror real-world fetch
// latency (and keep the lazy transition visible), show a brief "loading" state first and
// only then the "loaded" content.
document.querySelectorAll(".lazy-frame-demo").forEach((root) => {
const frame = root.querySelector('[data-controller~="stimeo--lazy-frame"]');
if (!frame) return;
// Idempotent: Turbo can re-run this inline module on navigation; wire each root once. The
// marker is a property, not an attribute: Turbo copies attributes into its page snapshot,
// so an attribute one comes back set on a restored page whose elements carry no listeners.
if (root.demoWired) return;
root.demoWired = true;
const loadedLabel = frame.dataset.loadedLabel ?? "Loaded:";
const loadingLabel = frame.dataset.loadingLabel ?? "Loading…";
frame.addEventListener("stimeo--lazy-frame:load", (event) => {
// Trigger fired (the library wrote src + set data-lazy-loaded). Simulate Turbo's
// network fetch so the placeholder -> loaded transition is perceptible on scroll.
frame.dataset.demoState = "loading";
frame.textContent = loadingLabel;
window.setTimeout(() => {
frame.dataset.demoState = "loaded";
frame.textContent = `${loadedLabel} ${event.detail.url}`;
}, 800);
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--lazy-frame"
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
url
|
ロードする URL。起動時にフレームの src へ書き写す(src ではなくここに保留)。 |
data-stimeo--lazy-frame-url-value |
rootMargin
|
IntersectionObserver のマージン(例 200px)。完全に見える前に早出しロードする。 | data-stimeo--lazy-frame-root-margin-value |
once
|
初回ロード後に監視を解除するか(既定 true)。false は再侵入で再ロード。 |
data-stimeo--lazy-frame-once-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
load
|
遅延ロードを起動したとき発火。detail.url を伴う。 |
stimeo--lazy-frame:load |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-lazy-loaded |
フレーム(コントローラ要素) | ロード起動済みのとき付与(true)。 |