Skip to content

blitz-dom: `get_client_bounding_rect` scrolls `position: fixed` boxes with the viewport

Generated from repros/blitz-dom-bounding-rect-fixed-scroll/README.md when the site was built at 7aa23e2.

crates/niso-core/src/api.rs:226-252: Dom::fixed_to_viewport walks the containing-block chain looking for a position: fixed box whose containing block is the root element, and Dom::rect adds viewport_scroll() back for such boxes and everything inside them.

BaseDocument::get_client_bounding_rect (packages/blitz-dom/src/document.rs:2451-2468) subtracts viewport_scroll() from every box. A box fixed to the viewport (and its descendants) does not move when the viewport scrolls, so its client rect must not change. Blitz’s own paint code already handles this (hoisted_child_position in layout/paint_tree.rs:39-51 cancels the viewport scroll for fixed boxes); the geometry query does not.

Viewport 800x600, body { height: 3000px }, a position: fixed; top: 10px box with a child, viewport scrolled to y = 100:

#fixed: expected y = 10, actual y = -90
#inner: expected y = 10, actual y = -90
Terminal window
CARGO_TARGET_DIR=~/.capy/work/repro-target cargo run

Exit status 1 while the bug reproduces, 0 once fixed. On Ubuntu, blitz needs sudo apt-get install -y libfontconfig-dev pkg-config.

Reproduces. Blitz main was at be60597f67b3cece0456d26e8d7ff5a88600bbc5 when tested (2026-11-08, git ls-remote https://github.com/DioxusLabs/blitz HEAD), the same commit niso pins, so the pinned result is also the main result.

None found. Searched DioxusLabs/blitz issues and PRs for getBoundingClientRect, get_client_bounding_rect, position fixed, scroll. Related but different: #874 (merged, unrounded positions for gBCR), #922 (merged, out-of-flow hoisting, fixed the paint side), #841 (fixed modal makes the viewport scrollable).

Title: get_client_bounding_rect subtracts the viewport scroll from position: fixed boxes

Body:

A position: fixed box (contained by the viewport) keeps its client rect when the viewport scrolls, as do its descendants. BaseDocument::get_client_bounding_rect subtracts viewport_scroll() from every box, so fixed boxes move up by the scroll offset. Paint already cancels this in hoisted_child_position.

use blitz_dom::{DocumentConfig, Point};
use blitz_html::HtmlDocument;
use blitz_traits::shell::{ColorScheme, Viewport};
const HTML: &str = r#"<!DOCTYPE html>
<html><body style="margin:0;height:3000px">
<div id="fixed" style="position:fixed;top:10px;left:0;width:50px;height:20px"><div id="inner" style="height:5px"></div></div>
</body></html>"#;
fn main() {
let config = DocumentConfig { viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)), ..Default::default() };
let mut doc = HtmlDocument::from_html(HTML, config);
doc.resolve(0.0);
doc.set_viewport_scroll(Point { x: 0.0, y: 100.0 });
doc.resolve(0.0);
for id in ["fixed", "inner"] {
let node = doc.get_element_by_id(id).unwrap();
println!("#{id}: {}", doc.get_client_bounding_rect(node).unwrap().y); // expected 10, actual -90
}
}

Tested at be60597f67b3cece0456d26e8d7ff5a88600bbc5. A fix: in get_client_bounding_rect (or physical_unrounded_geometry), skip the viewport scroll when the containing-block chain reaches a fixed box whose containing block is the root, the same test hoisted_child_position uses.