Bug description
Dragging the Live Preview resize handle by a almost always leaves the preview iframe unscrollable until something forces a content re-render or a deep layout change (e.g. sometimes crossing a breakpoint, typing a character in a content field, switching responsive device so the preview re-renders).
Safari is unaffected.
Video of the issue in action on clean Statamic install
https://www.loom.com/share/a0cb8fd2522d478f9fe704e67366d852
I've reproduced this on clean Statamic installs and multiple client sites.
How to reproduce
- Fresh Statamic v6.14.0 site or a site built with default Peak starter (reproduces on at least 3 separate Peak installs — also on a stripped-down bare-HTML layout, confirming the bug is upstream of any frontend code).
- Open any entry's Live Preview.
- Drag the resize handle 1–2 pixels, (sometimes crossing breakpoint will allow the scroll to work, small drags are worse than big ones).
- Release the mouse.
- Try to scroll the preview iframe.
It can be worked around with a dirty hack in the live preview to basically have a transparent box that is always printing updates based on a text mutation on each resize - but clearly that's not ideal.
{{ if live_preview || get:live-preview }}
{{# Chromium + Statamic Live Preview workaround. On a sub-breakpoint #}}
{{# iframe width change, Chromium fails to invalidate the iframe's #}}
{{# scroll state. A visible (in-viewport) element that receives a #}}
{{# `.value` mutation on each resize forces enough repaint for scroll #}}
{{# to survive. Off-screen / opacity-0 variants get optimized out by #}}
{{# Chromium and don't work. Safari doesn't need this. #}}
{{# Chromium + Statamic Live Preview drag-handle scroll workaround. #}}
{{# #}}
{{# Dragging the Live Preview resize handle by a pixel inside a #}}
{{# single Tailwind breakpoint leaves Chromium's iframe scroll state #}}
{{# stale — scroll becomes impossible until a content re-render or a #}}
{{# breakpoint cross forces a deep reflow. Safari is unaffected. #}}
{{# #}}
{{# Two things are needed to resolve it, determined empirically: #}}
{{# 1. An in-viewport textarea whose multi-line value grows on every #}}
{{# resize tick — Chromium's repaint of the textarea's internal #}}
{{# scrollbar is what actually unsticks the iframe's scroll. #}}
{{# Off-screen / opacity-0 / CSS-var variants did not work. #}}
{{# 2. A synthetic mouseup dispatched on the parent window 150 ms #}}
{{# after the drag ends — Statamic's Resizer.vue sometimes leaves #}}
{{# `pointer-events-none` stuck on .live-preview-contents, which #}}
{{# inherits into the iframe and blocks pointer events. #}}
{{# #}}
{{# The visible green box top-right is unfortunate but load-bearing. #}}
{{# Remove once fixed upstream in Statamic / Chromium. #}}
<textarea id="lp-scrollfix-debug" readonly aria-hidden="true" tabindex="-1" style="position:fixed;top:8px;right:8px;z-index:2147483647;background:transparent;color:transparent;font:10px monospace;padding:2px 4px;border:none;width:60px;height:20px;resize:none;white-space:pre;overflow:hidden;pointer-events:none"></textarea>
<script>
(() => {
const panel = document.getElementById('lp-scrollfix-debug');
const lines = [];
const push = (line) => {
lines.push(new Date().toLocaleTimeString().slice(3) + ' ' + line);
if (lines.length > 40) lines.shift();
if (panel) panel.value = lines.join('\n');
};
push('loaded');
if (typeof ResizeObserver === 'undefined') { push('ERROR: no ResizeObserver'); return; }
let timer = null;
let roN = 0, cleanupN = 0;
const cleanup = () => {
timer = null;
cleanupN++;
try {
const parentWin = window.parent;
const container = parentWin.document.querySelector('.live-preview-contents');
const hasClass = !!container?.classList.contains('pointer-events-none');
push(`cleanup #${cleanupN} — stuck=${hasClass}`);
if (hasClass) {
parentWin.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
setTimeout(() => {
const after = container.classList.contains('pointer-events-none');
push(` after mouseup: stuck=${after}`);
}, 50);
}
} catch (e) { push(`cleanup error: ${e.message}`); }
};
new ResizeObserver((entries) => {
roN++;
const r = entries[0].contentRect;
push(`RO#${roN} ${r.width.toFixed(0)}×${r.height.toFixed(0)}`);
if (timer) clearTimeout(timer);
timer = setTimeout(cleanup, 150);
}).observe(document.documentElement);
push('observer attached');
})();
</script>
{{ /if }}
Logs
Environment
Environment
Laravel Version: 13.6.0
PHP Version: 8.4.13
Composer Version: 2.9.5
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en
Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED
Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: log
Queue: sync
Session: file
Storage
public/storage: NOT LINKED
Statamic
Addons: 0
Sites: 1
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.14.0 Solo
Installation
Fresh statamic/statamic site via CLI
Additional details
No response
Bug description
Dragging the Live Preview resize handle by a almost always leaves the preview iframe unscrollable until something forces a content re-render or a deep layout change (e.g. sometimes crossing a breakpoint, typing a character in a content field, switching responsive device so the preview re-renders).
Safari is unaffected.
Video of the issue in action on clean Statamic install
https://www.loom.com/share/a0cb8fd2522d478f9fe704e67366d852
I've reproduced this on clean Statamic installs and multiple client sites.
How to reproduce
It can be worked around with a dirty hack in the live preview to basically have a transparent box that is always printing updates based on a text mutation on each resize - but clearly that's not ideal.
{{ if live_preview || get:live-preview }} {{# Chromium + Statamic Live Preview workaround. On a sub-breakpoint #}} {{# iframe width change, Chromium fails to invalidate the iframe's #}} {{# scroll state. A visible (in-viewport) element that receives a #}} {{# `.value` mutation on each resize forces enough repaint for scroll #}} {{# to survive. Off-screen / opacity-0 variants get optimized out by #}} {{# Chromium and don't work. Safari doesn't need this. #}} {{# Chromium + Statamic Live Preview drag-handle scroll workaround. #}} {{# #}} {{# Dragging the Live Preview resize handle by a pixel inside a #}} {{# single Tailwind breakpoint leaves Chromium's iframe scroll state #}} {{# stale — scroll becomes impossible until a content re-render or a #}} {{# breakpoint cross forces a deep reflow. Safari is unaffected. #}} {{# #}} {{# Two things are needed to resolve it, determined empirically: #}} {{# 1. An in-viewport textarea whose multi-line value grows on every #}} {{# resize tick — Chromium's repaint of the textarea's internal #}} {{# scrollbar is what actually unsticks the iframe's scroll. #}} {{# Off-screen / opacity-0 / CSS-var variants did not work. #}} {{# 2. A synthetic mouseup dispatched on the parent window 150 ms #}} {{# after the drag ends — Statamic's Resizer.vue sometimes leaves #}} {{# `pointer-events-none` stuck on .live-preview-contents, which #}} {{# inherits into the iframe and blocks pointer events. #}} {{# #}} {{# The visible green box top-right is unfortunate but load-bearing. #}} {{# Remove once fixed upstream in Statamic / Chromium. #}} <textarea id="lp-scrollfix-debug" readonly aria-hidden="true" tabindex="-1" style="position:fixed;top:8px;right:8px;z-index:2147483647;background:transparent;color:transparent;font:10px monospace;padding:2px 4px;border:none;width:60px;height:20px;resize:none;white-space:pre;overflow:hidden;pointer-events:none"></textarea> <script> (() => { const panel = document.getElementById('lp-scrollfix-debug'); const lines = []; const push = (line) => { lines.push(new Date().toLocaleTimeString().slice(3) + ' ' + line); if (lines.length > 40) lines.shift(); if (panel) panel.value = lines.join('\n'); }; push('loaded'); if (typeof ResizeObserver === 'undefined') { push('ERROR: no ResizeObserver'); return; } let timer = null; let roN = 0, cleanupN = 0; const cleanup = () => { timer = null; cleanupN++; try { const parentWin = window.parent; const container = parentWin.document.querySelector('.live-preview-contents'); const hasClass = !!container?.classList.contains('pointer-events-none'); push(`cleanup #${cleanupN} — stuck=${hasClass}`); if (hasClass) { parentWin.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); setTimeout(() => { const after = container.classList.contains('pointer-events-none'); push(` after mouseup: stuck=${after}`); }, 50); } } catch (e) { push(`cleanup error: ${e.message}`); } }; new ResizeObserver((entries) => { roN++; const r = entries[0].contentRect; push(`RO#${roN} ${r.width.toFixed(0)}×${r.height.toFixed(0)}`); if (timer) clearTimeout(timer); timer = setTimeout(cleanup, 150); }).observe(document.documentElement); push('observer attached'); })(); </script> {{ /if }}Logs
Environment
Installation
Fresh statamic/statamic site via CLI
Additional details
No response