Preview Guard
stimeo--preview-guard
Hides or placeholders a volatile element while Turbo is showing a cached preview.
The stimeo--preview-guard controller (Hotwire-specific) guards a volatile element — a balance, a notification count, a live timestamp — while Turbo is displaying a preview (html[data-turbo-preview]), so a stale cached snapshot does not briefly flash the old value on a back/restore visit. It watches <html> for the data-turbo-preview attribute with a MutationObserver: while it is present, mode="hide" makes the element visibility:hidden (its box is kept, so nothing shifts) and mode="placeholder" swaps its text for the placeholder; the element carries data-preview-hidden and emits hide. When the preview clears it restores the saved value and emits show. Behavior only — fetching the fresh value is the normal render's job, not this controller's. A guard that connects mid-preview hides immediately, focus is never moved, and the observer is severed (and the element restored) on disconnect (Turbo navigation included).
- Balance
- ¥123,456
- Updated
- 12:34:56
While Turbo briefly shows a cached snapshot on a back/forward visit, the balance becomes a placeholder and the timestamp is hidden — so no stale value flashes (the button simulates that ~1.5s window).
<%# Preview-guard demo: data-turbo-preview is a Turbo-internal attribute, so this catalog
has no real preview to show. demo.js toggles it on <html> for ~1.5s to reproduce a
preview window — exactly what Turbo does on a back/restore visit. The balance guards in
placeholder mode (swaps to —) and the timestamp in hide mode (becomes invisible, box
kept). The library only watches the attribute and reflects data-preview-hidden. %>
<div class="preview-guard-demo">
<button type="button" class="demo-trigger" data-preview-guard-demo-toggle>
<%= t("components.preview_guard.demo.toggle") %>
</button>
<dl class="preview-guard-demo__list">
<div class="preview-guard-demo__row">
<dt><%= t("components.preview_guard.demo.balance_label") %></dt>
<dd>
<span
class="preview-guard-demo__value"
data-controller="stimeo--preview-guard"
data-stimeo--preview-guard-mode-value="placeholder"
data-stimeo--preview-guard-placeholder-value="—"
><%= t("components.preview_guard.demo.balance") %></span>
</dd>
</div>
<div class="preview-guard-demo__row">
<dt><%= t("components.preview_guard.demo.updated_label") %></dt>
<dd>
<span
class="preview-guard-demo__value"
data-controller="stimeo--preview-guard"
><%= t("components.preview_guard.demo.updated") %></span>
</dd>
</div>
</dl>
<p class="preview-guard-demo__note"><%= t("components.preview_guard.demo.note") %></p>
</div>
/*
* Presentation-only styles for the preview-guard demo. The library swaps the text
* (placeholder mode) or sets visibility:hidden (hide mode) and reflects
* data-preview-hidden; this CSS only lays out the rows and tints a guarded value so the
* placeholder swap is easy to spot.
*/
.preview-guard-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 24rem;
}
.preview-guard-demo__list {
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.preview-guard-demo__row {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
}
.preview-guard-demo__row dt {
color: var(--color-text-muted);
}
.preview-guard-demo__row dd {
margin: 0;
font-variant-numeric: tabular-nums;
}
.preview-guard-demo__value[data-preview-hidden] {
color: var(--color-text-subtle);
}
.preview-guard-demo__note {
margin: 0;
color: var(--color-text-muted);
}
// Preview-guard demo (consumer-side JS).
//
// Turbo sets html[data-turbo-preview] itself while a cached preview is on screen; this
// catalog has no Turbo navigation, so the button toggles that attribute for ~1.5s to
// reproduce the preview window. Every preview-guard on the page reacts (just as they
// would during a real preview), guarding their volatile values until it clears.
document.querySelectorAll(".preview-guard-demo").forEach((root) => {
const button = root.querySelector("[data-preview-guard-demo-toggle]");
if (!button) return;
// Idempotent: Turbo can re-run this inline module on navigation; wire each root once.
if (root.dataset.demoWired) return;
root.dataset.demoWired = "1";
button.addEventListener("click", () => {
document.documentElement.setAttribute("data-turbo-preview", "");
setTimeout(() => {
document.documentElement.removeAttribute("data-turbo-preview");
}, 1500);
});
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--preview-guard"
Values
| Name | Description | Attribute |
|---|---|---|
placeholder
|
Text shown in placeholder mode while guarded (default empty). |
data-stimeo--preview-guard-placeholder-value |
mode
|
hide (visibility:hidden, keeps the box) or placeholder (swap the text). Default hide. |
data-stimeo--preview-guard-mode-value |
Events
| Name | Description | Event |
|---|---|---|
hide
|
Fires when the element is guarded as a preview begins. | stimeo--preview-guard:hide |
show
|
Fires when the element is restored as the preview clears. | stimeo--preview-guard:show |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-preview-hidden |
Controller element | Present (true) while guarded during a preview. |