Fix HTML Viewer Toolbar Collapse and Reveal UX
Summary
The HTML viewer toolbar is currently behaving according to its timer-based implementation, but that behavior feels delayed and visually inconsistent with the Markdown viewer.
The Markdown viewer can observe its own document scroll position, so it hides after deliberate downward scrolling and reappears after upward scrolling. HTML artifacts scroll inside an opaque sandbox="" iframe. The trusted parent viewer cannot inspect the iframe scroll position or direction, and iframe scroll events do not bubble into the parent.
The safe v1 fix is therefore not to imitate scroll-direction detection. Instead, make the HTML toolbar a fast, deterministic overlay that collapses independently of iframe loading, does not resize the artifact, and can be recalled through a minimal accessible top-edge activation area.
This plan keeps the existing security boundary intact:
- Keep the artifact iframe at
sandbox="". - Keep published HTML and
/rawatscript-src 'none'. - Do not inject a scroll bridge into artifact HTML.
- Do not inspect
iframe.contentDocumentoriframe.contentWindow.document. - Do not intercept iframe wheel or touch events with an overlay.
What was observed
Markdown reference
- The toolbar is visible near the top.
- Deliberate downward scrolling hides the complete toolbar.
- Upward scrolling reveals it immediately.
- The document content does not jump when toolbar visibility changes.
HTML reference
- The parent toolbar waits for the iframe to finish loading.
- It then waits an additional
1800msbefore collapsing. - Scrolling down or up inside the artifact does not affect toolbar state.
- Recalling the toolbar starts a separate
1400msre-hide delay. - The iframe height changes from
calc(100dvh - 3rem)to100dvh, which can cause artifact viewport reflow during the transition.
The sun/moon pill visible at the top-right after collapse is not part of the agent-pages toolbar. It is an author-owned theme switch inside the published HTML artifact. The viewer-owned remnant is the small centered top-edge reveal handle.
Root cause
1. HTML visibility is timer-driven
app/static/viewer-toolbar.js defines:
var HTML_INITIAL_HIDE_DELAY_MS = 1800;
var HTML_REHIDE_DELAY_MS = 1400;
The initial timer is armed only by markIframeLoaded(). Because iframe load can wait for artifact resources such as remote images, actual collapse time is:
artifact load duration + 1800ms
This is why the toolbar appears slow to respond.
2. The parent cannot detect artifact scroll direction
The artifact is loaded through:
<iframe sandbox="" ...></iframe>
The empty sandbox gives the document an opaque origin and prevents scripts from running. The parent cannot safely read the artifact scroll position or attach a listener to its document. Scroll and wheel activity inside the nested browsing context does not bubble to the parent shell.
Therefore, exact Markdown-style “scroll down to hide, scroll up to reveal” cannot be implemented under the locked v1 security model.
3. Toolbar state changes resize the iframe
app/static/viewer.css currently changes both the host padding and iframe height:
body.viewer-html-host .viewer-main.viewer-main-iframe {
padding: var(--toolbar-height) 0 0;
}
body.viewer-html-host .viewer-iframe {
height: calc(100dvh - var(--toolbar-height));
}
body.viewer-html-host.viewer-toolbar-collapsed .viewer-main.viewer-main-iframe {
padding-top: 0;
}
body.viewer-html-host.viewer-toolbar-collapsed .viewer-iframe {
height: 100dvh;
}
Changing the iframe viewport height can re-run responsive layout inside the artifact and makes the transition feel less stable.
4. Existing tests encode the current implementation
The HTML tests assert the exact 1800ms and 1400ms constants and use source-string checks for load handling. They do not exercise the full visible → collapsed → revealed → collapsed interaction as browser behavior.
Desired outcome
Markdown
Leave Markdown behavior unchanged:
- Visible near the top.
- Hide after
64pxof accumulated downward scrolling. - Reveal after
32pxof accumulated upward scrolling. - Remain visible while a toolbar control has focus or a menu is open.
- Keep the existing content spacing and non-jumping document layout.
HTML
Adopt a secure overlay model:
- Render the toolbar immediately with the shell.
- Collapse it within approximately one second of the shell’s first paint.
- Do not wait for iframe
loador remote artifact resources. - Keep the iframe at a constant full-viewport size in both toolbar states.
- Overlay the toolbar on top of the artifact instead of reserving and recovering iframe height.
- Reveal the toolbar through a narrow top-edge activation zone.
- Keep it visible while the pointer or focus is inside the toolbar or a menu is open.
- Collapse shortly after interaction ends.
- Keep JavaScript failure behavior safe: toolbar remains visible and usable.
Implementation plan
1. Replace iframe-load-gated initialization
Update app/static/viewer-toolbar.js.
Remove iframeLoaded as a prerequisite for initial collapse and interaction re-hide. The HTML controller should arm from trusted parent-shell initialization.
Recommended constants:
var HTML_INITIAL_HIDE_DELAY_MS = 900;
var HTML_REHIDE_DELAY_MS = 600;
After showToolbar():
- Wait until the parent has completed at least one paint using
requestAnimationFrame. - Schedule the initial hide timer.
- Apply the existing visibility guards before hiding.
Conceptually:
showToolbar();
requestAnimationFrame(function () {
scheduleHide(HTML_INITIAL_HIDE_DELAY_MS);
});
Do not make iframe load restart or postpone this timer. The listener can be removed entirely unless it serves a separately documented recovery purpose.
pageshow should restore a deterministic visible state and schedule the same parent-owned initial timer.
2. Introduce an explicit HTML toolbar state model
Keep the Markdown scroll reducer unchanged.
For HTML, centralize transitions around these states and events:
visiblecollapsedINITIAL_TIMEOUTREVEAL_STARTREVEAL_ENDTOOLBAR_INTERACTION_STARTTOOLBAR_INTERACTION_ENDMENU_OPENEDMENU_CLOSEDPAGESHOW
State application must be the only place that changes:
.viewer-toolbar-hiddenaria-hidden- toolbar
inert - reveal-control
hidden - reveal-control
aria-expanded
This prevents timers, pointer handlers, and focus handlers from producing contradictory DOM state.
3. Keep the HTML iframe viewport constant
Update app/static/viewer.css so the HTML iframe is always full viewport:
body.viewer-html-host .viewer-main.viewer-main-iframe {
position: fixed;
inset: 0;
max-width: none;
margin: 0;
padding: 0;
}
body.viewer-html-host .viewer-iframe {
display: block;
width: 100%;
height: 100vh;
height: 100dvh;
margin: 0;
border: 0;
border-radius: 0;
}
Remove the HTML-specific transitions that animate:
padding-top- iframe
height - collapsed iframe height overrides
Keep the toolbar fixed above the iframe with its opaque themed surface. It may cover the top 3rem of the artifact while visible. When hidden, only the toolbar itself moves; the artifact viewport and scroll position remain unchanged.
The body-level collapsed class may be retained for reveal-control styling, but it must no longer resize the iframe.
4. Refine the top-edge reveal control
Keep the HTML-only semantic button in app/templates/viewer/html_host.html.
For fine-pointer devices:
- Make the activation zone span the top edge with a maximum height of approximately
6–8px. - Keep it visually transparent while idle.
- Display the centered line only on hover or
:focus-visible. - Do not cover meaningful artifact content below the activation strip.
For coarse-pointer devices:
- Retain a visible centered handle with a practical touch target.
- Keep its visual footprint small.
- Do not make the entire top edge a large invisible touch overlay.
Accessibility requirements:
- Keep
type="button". - Keep
aria-controls="viewer-toolbar". - Set
aria-expanded="true"only while the toolbar is visible. - Keep a clear
:focus-visiblering. - Clicking or focusing the control must move focus into the toolbar without trapping it.
- The hidden toolbar must be
inertandaria-hidden="true".
5. Preserve interaction guards
The toolbar must not collapse while any of these is true:
- The pointer is over the toolbar.
- The toolbar contains
document.activeElement. - The version
<details>menu is open. - The theme
<details>menu is open. - The reveal control is hovered or focused.
When the final guard ends, cancel any stale timer and schedule the shorter re-hide delay.
The auto-hide controller must not close menus itself.
6. Separate viewer chrome from artifact-owned UI
Do not attempt to remove or restyle the sun/moon pill in the reference HTML artifact. It exists inside the sandboxed /raw document and is part of the published page.
Add a neutral HTML viewer fixture without a built-in fixed header or theme control. Use that fixture for host-toolbar browser screenshots and acceptance checks.
If the reference artifact should no longer contain its own theme pill, update that artifact’s source and publish a new page version as a separate content change.
7. Update automated tests
JavaScript state tests
Update tests/test_viewer_toolbar.py to:
- Assert the new initial and re-hide delays.
- Remove tests that require iframe
loadto arm collapse. - Verify initialization schedules collapse without iframe load.
- Verify initial timeout collapses only when no guard is active.
- Verify reveal hover, focus, and click show the toolbar.
- Verify interaction cancels stale timers.
- Verify leaving interaction schedules exactly one re-hide timer.
- Verify open menus and toolbar focus prevent collapse.
- Verify
pageshowrestores visible state and schedules a fresh timer. - Verify class,
aria-hidden,inert, andaria-expandedstay synchronized.
Use fake timers or a pure reducer/controller harness. Do not make tests wait for real timeout durations.
CSS tests
Update tests/test_viewer_css.py to verify:
- HTML iframe height is always
100vhand100dvh. - HTML main has no toolbar-dependent padding.
.viewer-toolbar-collapseddoes not change iframe height.- Only toolbar opacity/transform animate.
- The reveal activation zone is bounded.
- Fine-pointer and coarse-pointer reveal styles are distinct.
- Reduced-motion removes toolbar/reveal animation without disabling state changes.
Template and security tests
Preserve assertions that:
- HTML uses
sandbox=""exactly. - Published HTML is absent from the parent DOM.
/rawretainsscript-src 'none'.- The host shell permits only trusted same-origin toolbar JavaScript.
- The iframe source and fullscreen link remain pinned to the immutable resolved version.
- Markdown does not render the HTML reveal control.
8. Browser verification
Test in clean Safari and Chromium sessions at desktop and mobile-sized viewports.
HTML checklist
- Toolbar and artifact paint immediately.
- Toolbar collapses within approximately one second of shell paint.
- Slow or failed remote images do not postpone collapse.
- The complete viewer toolbar slides out; no title, version, theme, or fullscreen control remains.
- The iframe dimensions do not change during show/hide.
- Artifact scroll position is preserved.
- Responsive artifact layout does not reflow during toolbar transitions.
- Top-edge hover reveals the toolbar on desktop.
- Keyboard focus and activation reveal it.
- Touch activation works on coarse-pointer emulation.
- Menus and focused controls remain visible during interaction.
- Re-hide occurs shortly after interaction ends.
- JavaScript-disabled behavior leaves the toolbar visible.
- The artifact’s own fixed controls remain untouched.
Markdown regression checklist
- Near-top toolbar remains visible.
- Downward scrolling hides it at the existing threshold.
- Upward scrolling reveals it at the existing threshold.
- Menus and focus keep it visible.
- Content spacing and scroll position do not change.
Documentation changes
Update:
MEMORY.mddecision D36.docs/architecture.mdviewer UX description.AGENTS.mdHTML viewer summary if timing or overlay wording changes.
Document the HTML behavior as:
The HTML host uses a parent-shell overlay toolbar that collapses shortly after the shell paints and is recalled through a top-edge control. The iframe remains full viewport and is never resized by toolbar visibility. Scroll direction inside the opaque sandboxed artifact remains unobservable.
Do not claim that HTML visibility follows artifact scroll direction.
Explicit non-goals
Do not:
- Add
allow-scriptsorallow-same-originto the artifact iframe. - Change
/rawto permit scripts. - Inject a scroll bridge into published HTML.
- Use
iframe.contentDocumentor inspect the artifact DOM. - Intercept iframe wheel/touch gestures with a large overlay.
- Remove or restyle controls authored inside published HTML.
- Change Markdown thresholds or layout.
- Expand the viewer into editing or artifact navigation features.
Acceptance criteria
The work is complete when:
- HTML toolbar collapse begins from the trusted shell rather than iframe
load. - HTML toolbar collapses within approximately one second even when artifact resources load slowly.
- HTML toolbar visibility never changes iframe height or viewport dimensions.
- The entire viewer toolbar disappears; only the appropriate top-edge reveal affordance remains.
- Toolbar reveal works with pointer, keyboard, and touch.
- Toolbar menus and focused controls never disappear mid-interaction.
- The reference artifact’s sun/moon pill is correctly recognized as artifact-owned content.
- Markdown scroll behavior is unchanged.
sandbox="", rawscript-src 'none', CSP, immutable version pinning, and fullscreen behavior remain intact.- Automated tests and Safari/Chromium verification pass.
Future decision if exact scroll parity is required
Exact “scroll down inside HTML hides, scroll up reveals” requires cooperation from code executing inside the artifact browsing context. Achieving that would require a trusted scroll bridge, a different sandbox capability set, transformed artifact responses, and a new message-validation/security model.
That is a material architecture and security change which conflicts with the locked v1 decisions. It should be evaluated separately as a v1.1 proposal and must not be introduced as part of this toolbar UX fix.