DioxusLabs / DioxusLabs/blitz

Viewport becomes scrollable (and scrolls the whole document, mask included) when a fixed, translate-centered modal is taller than half the window — scrollable overflow ignores transforms

Open
#841 5 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
4.1k
Forks
203
Avg merge
8h 58m
Merged PRs (30d)
112

Description

## Summary

A modal positioned with `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%)` makes the **viewport** scrollable whenever the modal is taller than half the window. A wheel scroll then moves `viewport_scroll`, which translates the **entire** document — the fixed mask included — leaving blank space below and a dead hit-test region where the cursor disappears. Per CSS the viewport must not scroll at all: the modal paints entirely inside the window.

The scrollable distance is exactly `modal_height − window_height / 2`, which is the part of the modal that sticks out below the window **before** the translate is applied.

## Minimal reproduction

Complete runnable example (`blitz::launch_static_html`):

```rust
const HTML: &str = r#"




html, body { border:0; margin:0; padding:0; width:100vw; height:100vh; }
* { box-sizing: border-box; }
.mask {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.2);
}
.modal {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 300px; height: 90vh; overflow: auto; background: #fff;
}







"#;

fn main() {
blitz::launch_static_html(HTML);
}
```

Roll the wheel anywhere: the mask and body move up together by `40vh` (90vh modal, half-window offset), blank space appears below, and the cursor disappears over the blank region. Removing only the `transform` line changes the behavior to correct scrolling-by-real-overflow (the modal then genuinely sticks out).

The trigger is not specific to modals or to this geometry: any box whose border box crosses the root's padding edge **before** its transform is applied, and is pulled back inside by that transform, grants the viewport phantom scroll range equal to the pre-transform overshoot. Verified with fixed, absolute and in-flow boxes alike; the `top: 50%` + `translate(-50%, -50%)` modal is just the shape real apps hit.

## Analysis

`scroll_state` in `blitz-dom/src/scrolling.rs` reads the **layout-time** overflow:

- the viewport branch uses `root.final_layout().scrollable_overflow_rect` (and the node branch uses `final_layout().scroll_height()`, same source);
- these values are computed without any knowledge of transforms, so the modal contributes its **pre-translate** bottom edge (`window/2 + modal_height`) to the root overflow.

Blitz already computes the transform-aware bounds: `resolve_transforms` in `blitz-dom/src/resolve.rs` applies `Affine::translate(location) * node.transform` to child contributions and stores the result in `ElementData::scrollable_overflow()` (used by paint and hit-testing). Scrolling is the only consumer still reading the pre-transform value. Measured on a 100×100 viewport with a 40×64 modal:

| source | translated | untranslated |
|---|---|---|
| layout-time `scrollable_overflow_rect.bottom` (used by `scroll_state`) | **114 (wrong)** | 114 |
| transform-aware `ElementData::scrollable_overflow()` | **100 (correct)** | 114 |

css-overflow-3 §3.3 requires the scrollable overflow area to be calculated **accounting for transforms** — each contributing box enters with its transform applied. The modal's border box therefore lies entirely within the window, and it should add nothing to the root's scrollable overflow. The layout-time value used by `scroll_state` instead reflects the modal's position before `translate(-50%, -50%)`, granting the viewport `modal_height − window_height/2` of scroll range that corresponds to nothing on screen.

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue with the provided blitz::launch_static_html example, then read blitz-dom/src/scrolling.rs and resolve.rs to trace the two overflow calculations. Compare the viewport and node scroll-state inputs with ElementData::scrollable_overflow() for transformed and untransformed boxes. Done means the translated modal adds no phantom viewport scroll range, the mask remains fixed during wheel input, and genuine overflow still scrolls correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, rust
Domain
frontend, web-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.