Skip to content

Fix #12865: prevent toolbar clipping in containers#14471

Closed
TiagoLouro14 wants to merge 5 commits intostreamlit:developfrom
TiagoLouro14:TiagoLouro14
Closed

Fix #12865: prevent toolbar clipping in containers#14471
TiagoLouro14 wants to merge 5 commits intostreamlit:developfrom
TiagoLouro14:TiagoLouro14

Conversation

@TiagoLouro14
Copy link
Copy Markdown

Describe your changes

Fixes the toolbar being clipped inside fixed-height containers (st.container with height). Elements like st.dataframe, st.image, and st.map render a hover toolbar that was being cut off by the container's overflow: hidden.

The fix renders the toolbar in a position: fixed overlay that mirrors the parent element's bounding box, bypassing the overflow constraint. A ResizeObserver and scroll/resize listeners keep the overlay synchronized with the parent.

Screenshot or video (only for visual changes)

vitoria_topo

GitHub Issue Link (if applicable)

Fixes #12865

Testing Plan

  • Unit Tests (JS): Added "uses fixed positioning outside of fullscreen mode" to Toolbar.test.tsx, verifying that the wrapper uses position: fixed and pointer-events: none outside fullscreen mode.
  • E2E Tests: Added st_toolbar_overflow.py (app) and st_toolbar_overflow_test.py (Playwright test) that reproduce the clipping scenario and assert position: fixed on the toolbar wrapper.
  • Manual testing: Hover over a dataframe at the top of a fixed-height st.container — toolbar is now fully visible.

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

Implements a fixed-position overlay strategy for the toolbar
to ensure visibility inside restricted containers. This fix
allows the menu to overlap borders by using a portal-like
approach with ResizeObserver for synchronization.

Signed-off-by: Tiago Louro <tiago.louro@tecnico.ulisboa.pt>
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Mar 23, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

Thanks for contributing to Streamlit! 🎈

Please make sure you have read our Contributing Guide. You can find additional information about Streamlit development in the wiki.

The review process:

  1. Initial triage: A maintainer will apply labels, approve CI to run, and trigger AI-assisted reviews. Your PR may be flagged with status:needs-product-approval if the feature requires product team sign-off.

  2. Code review: A core maintainer will start reviewing your PR once:

    • It is marked as 'ready for review', not 'draft'
    • It has status:product-approved (or doesn't need it)
    • All CI checks pass
    • All AI review comments are addressed

We're receiving many contributions and have limited review bandwidth — please expect some delay. We appreciate your patience! 🙏

@lukasmasuch lukasmasuch added change:bugfix PR contains bug fix implementation impact:users PR changes affect end users labels Mar 23, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR fixes an issue (#12865) where the element toolbar is clipped when rendered inside a fixed-height container with overflow: hidden. The fix renders the toolbar within a position: fixed wrapper that mirrors the parent element's bounding box, kept in sync via scroll, resize, and ResizeObserver listeners. It includes a new Playwright e2e test and a frontend unit test for the fix.

Code Quality

The approach of using a fixed-position overlay to escape overflow: hidden clipping is sound in principle, but the current implementation has a significant performance concern. The Toolbar component is used by many element types (DataFrames, images, maps, charts, etc.), and the current design attaches global scroll and resize event listeners per Toolbar instance. On every scroll event, getBoundingClientRect() is called (forcing synchronous layout) and setParentRect creates a new DOMRect object. Because React's useState uses strict equality, this triggers a re-render for every visible Toolbar on every scroll frame — a pattern that will cause noticeable scroll jank on pages with multiple elements. Both reviewers flagged concerns about the robustness and performance of the synchronization logic. This is the primary blocking issue.

The code is otherwise cleanly scoped, follows existing component patterns, and properly handles the fullscreen vs. standard mode distinction. Cleanup for listeners and the ResizeObserver is correctly implemented.

Test Coverage

Test coverage is directionally good:

  • A new frontend unit test validates the fixed-wrapper positioning behavior in non-fullscreen mode.
  • A new Playwright e2e test reproduces the clipped-toolbar scenario with a constrained container.

However, the e2e test assertion (expect(wrapper).to_have_css("position", "fixed")) is implementation-oriented rather than behavior-oriented. A stronger assertion would verify that the toolbar is actually visible/unclipped from the user's perspective.

Backwards Compatibility

No API or protobuf surface changes. The change is internal to the shared Toolbar UI component. The primary regression risk is visual/positioning issues in existing toolbar consumers, and performance degradation on scroll-heavy pages.

Security & Risk

No security-sensitive areas (auth, WebSocket, CORS, CSP, file handling, etc.) are affected. No new dependencies or external network calls introduced. The risk is confined to UI performance and visual correctness.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence: UI-only change for toolbar layout positioning. No routing, auth, WebSocket, asset handlers, CORS, CSP, storage, embedding, or SiS/runtime changes.
  • Suggested external_test focus areas: None required. Optional confidence check: verify toolbar placement inside an iframe-hosted app with nested scroll containers.
  • Confidence: High
  • Assumptions and gaps: Assessment is based on the diff alone.

Accessibility

No accessibility regressions introduced. The toolbar retains its existing structure, interactive elements, and accessible labels. No new interactive controls or semantic-role changes.

Recommendations

  1. Critical — Avoid React state for scroll-driven positioning: Refactor the position synchronization to use a ref for the fixed-wrapper div and update style.top, style.left, style.width, style.height directly, bypassing React re-renders. Use requestAnimationFrame to batch DOM reads/writes.
  2. Important — Lazy listener attachment: Only attach global scroll and resize listeners when the parent element is hovered or the toolbar is locked. This avoids tracking and layout calculations for toolbars that are invisible.
  3. Minor — Guard against stale coordinates on content reflow: Recompute position on mouseenter of the parent element (or another layout-shift-safe trigger) to cover cases where content reflows without triggering scroll/resize.
  4. Minor — Strengthen e2e assertion: Assert user-visible behavior (e.g., toolbar visibility or bounding-box geometry) rather than only CSS position: fixed.

Verdict

CHANGES REQUESTED: The approach correctly solves the toolbar-clipping problem, but the scroll-driven React state updates introduce a significant performance regression that must be addressed before merge. Both reviewers independently flagged this or related robustness concerns. The fix should use direct DOM style manipulation via refs instead of React state for high-frequency position synchronization.


Reviewer agreement:

  • All reviewers (gemini-3.1-pro, gpt-5.3-codex-high) agree on CHANGES_REQUESTED.
  • Both agree the fix is sound in principle but needs implementation hardening.
  • Both agree there are no security, accessibility, or backwards compatibility concerns.
  • Gemini focused primarily on the performance regression; GPT focused on stale-position robustness and e2e test quality. Both concerns are valid and complementary.

Missing reviews: None. All expected models (gpt-5.3-codex-high, gemini-3.1-pro) submitted reviews. claude-4.6-opus-high-thinking served as the consolidation model.


Consolidated AI review by claude-4.6-opus-high-thinking. Please verify the feedback and use your judgment.

This review also includes 3 inline comment(s) on specific code lines.

const updatePosition = (): void => {
if (anchorRef.current?.parentElement) {
// eslint-disable-next-line streamlit-custom/no-force-reflow-access
setParentRect(anchorRef.current.parentElement.getBoundingClientRect())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Calling getBoundingClientRect() and setParentRect() with a new DOMRect on every scroll event triggers a React re-render for every Toolbar instance on the page (since useState uses === and each DOMRect is a new object). With many elements (DataFrames, images, maps, etc.), this causes severe scroll jank.

Suggestion: Use a useRef for the fixed-wrapper div and update its style.top/left/width/height directly (bypassing React state). Additionally, throttle updates with requestAnimationFrame, and consider only attaching scroll/resize listeners when the parent is hovered or the toolbar is locked, so invisible toolbars don't incur tracking cost.

updatePosition()

window.addEventListener("resize", updatePosition)
window.addEventListener("scroll", updatePosition, true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The fixed-overlay position is only recomputed on resize, scroll, and parent ResizeObserver events. If the element moves because surrounding content reflows (e.g., content expansion above the element) without triggering scroll or resize, the toolbar stays at stale coordinates. Consider recomputing position right before showing the toolbar (e.g., on mouseenter of the parent) as an additional trigger to avoid misalignment.

wrapper = toolbar.locator("xpath=..")

# Assert the fix: without position: fixed, this would be invisible/clipped
expect(wrapper).to_have_css("position", "fixed", timeout=10000)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Asserting only position: fixed verifies the implementation detail but can still pass when the toolbar is clipped or offscreen. Consider also asserting visible geometry (e.g., that the toolbar's bounding box is within the viewport or that it is actually visible via expect(toolbar).to_be_visible()) so the test validates user-visible behavior.

@github-actions github-actions bot added the do-not-merge PR is blocked from merging label Mar 23, 2026
@sfc-gh-lwilby sfc-gh-lwilby self-assigned this Mar 26, 2026
@sfc-gh-lwilby
Copy link
Copy Markdown
Collaborator

@TiagoLouro14 this is something I already have a draft PR for taking a different approach via portals. That's why I had the issue assigned to myself.

@TiagoLouro14
Copy link
Copy Markdown
Author

@TiagoLouro14 this is something I already have a draft PR for taking a different approach via portals. That's why I had the issue assigned to myself.

Sorry about that, I missed the assignment notice and jumped the gun. I’m doing this for a university project and just wanted to contribute to anything streamlit relaated. I’m happy to close this since you’ve already got a draft with portals in the works. Let me know if I can help with anything else! ;)

@sfc-gh-lwilby
Copy link
Copy Markdown
Collaborator

@TiagoLouro14 no problem, sometimes assigned issues can be re-assigned. Just ping the person to check.

One thing we are really encouraging people to take a look at is implementing a custom component that they design. We have recently rolled out V2 and a new gallery.

https://docs.streamlit.io/develop/concepts/custom-components

I am wondering actually, we get a lot of contributions from your University, could you let me know who your instructor is? I want to talk to them about custom components.

One issue with submitting bug fixes or features is that our team is handling these much faster with agents so there are not a lot of available issues that are good to hand off to new contributors.

@TiagoLouro14
Copy link
Copy Markdown
Author

@sfc-gh-lwilby, Thanks for the heads up!

Since you already have a draft PR, does it make sense for me to hand this over to you? I don't want to get in your way if your progress is really substantial.

I'll definitely give the custom components V2 a serious look for my project. About the instructor and other details is there a way I can send you more details or his contact in private?

@sfc-gh-lwilby
Copy link
Copy Markdown
Collaborator

@TiagoLouro14 the approach here has some performance implications that my portal approach does not: #11748

My PR is a bit old, if you want, you could re-make it against the current develop, there are some review suggestions from Bob that also need to be addressed. But keep in mind this one will require some manual testing and there are some mobile concerns raised by Bob so ideally we could verify the fix works in mobile as well.

I am not working on it for a few weeks, so let me know if you want to do this, ping me on your PR and I will take a look (I will be on vacation until after Easter).

Could you send me a message on LinkedIn? Seems like the best contact to link here https://www.linkedin.com/in/laura-wilby-03508316/

@TiagoLouro14
Copy link
Copy Markdown
Author

Sure, I'll give it a go.
I'll make sure to update it against the current develop branch and I’ll try my best to verify the mobile concerns as well.
When I'm done ill ping you right away, enjoy your vacation ;)
Sent the connection with a message on linkedin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation do-not-merge PR is blocked from merging impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fullscreen menu partially hidden in fixed height containers.

3 participants