blitz-dom: an inline element's client rect uses the line box height, not its content area
Generated from repros/blitz-dom-inline-rect-line-height/README.md when the site was built at 7aa23e2.
What niso does
Section titled “What niso does”Found by Acid2 (the .chin div, whose span is measured). crates/niso-core/src/geometry.rs:
Dom::inline_rect measures non-atomic inlines for Element.getBoundingClientRect() itself, each fragment spanning
its glyph runs’ font ascent and descent around their baseline. Inline boxes have no native view of their own (they
render as part of their inline formatting context’s text), so nothing else uses blitz’s rect.
Where the bug is
Section titled “Where the bug is”blitz-dom Node::inline_fragment_boxes (packages/blitz-dom/src/node/node.rs:1789-1886), used by
get_client_bounding_rect for non-atomic inlines (document.rs:2451-2456, inline_fragment_rects at
document.rs:2545). For each glyph run it takes the line’s block_min_coord/block_max_coord
(node.rs:1854-1860), on purpose (“Use the line box’s block extent rather than the run’s font ascent/descent …
matches the geometry used for text selection highlights”). CSSOM’s getBoundingClientRect for an inline box is
its border box, whose height is the content area of its own font (CSS 2.1 §10.6.1), placed around its baseline,
not the line box. So the selection geometry and the CSSOM geometry need different numbers.
Expected vs actual
Section titled “Expected vs actual”<p style="width:100px;font:20px/40px sans-serif">x <span style="font:2px/4px serif"> </span> y</p>:
span with font 2px/4px on a 40px lineexpected: height = content area of a 2px font (Chrome: y=25 height=3), at most 4actual: y=0 height=40Chrome 156: span y=25, height 3. The repro checks only height <= 4 (the span’s own line-height bounds its content
area), since the exact content-area height depends on the serif font installed.
CARGO_TARGET_DIR=~/.capy/work/repro-target cargo runExit status 1 while the bug reproduces, 0 once fixed. Needs libfontconfig-dev pkg-config on Ubuntu. Built with
blitz-dom features system-fonts, accessibility, floats (as niso), viewport 800x600 at scale 1.
Upstream main
Section titled “Upstream main”Reproduces. Blitz main was still at be60597f67b3cece0456d26e8d7ff5a88600bbc5 (niso’s pin) when tested on 2026-11-08, so the pinned result is the main result. Also still reproduces with PR #1370 applied (0c20c4e).
Existing issue or PR
Section titled “Existing issue or PR”None found (searched blitz issues/PRs for getBoundingClientRect, inline, line-height, fragment). #874 (merged,
unrounded gBCR positions), #1060/#1062 (merged, moved this code to Node::inline_fragment_boxes) introduced or moved
the current behaviour.
Issue text
Section titled “Issue text”Title: get_client_bounding_rect of an inline element is the line box’s height, not the inline box’s
Body:
For non-atomic inlines, Node::inline_fragment_boxes uses the line box’s block extent for every glyph run. That
suits selection highlights, but getBoundingClientRect/getClientRects of an inline box is its border box: the
content area of the element’s own font (ascent + descent, CSS 2.1 §10.6.1) around its baseline, plus padding and
border. A small inline on a tall line reports the whole line.
use blitz_dom::DocumentConfig;use blitz_html::HtmlDocument;use blitz_traits::shell::{ColorScheme, Viewport};
const HTML: &str = r#"<body style="margin:0"><p style="width:100px;font:20px/40px sans-serif;margin:0">x <span id="s" style="font:2px/4px serif"> </span> y</p></body>"#;
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); let r = doc.get_client_bounding_rect(doc.get_element_by_id("s").unwrap()).unwrap(); println!("y={} height={}", r.y, r.height); // Chrome 156: y=25 height=3; blitz: y=0 height=40}Tested at be60597. Suggested fix: keep the line extent for selection, and for CSSOM rects use
baseline - ascent .. baseline + descent of the run’s font metrics (glyph_run.baseline(),
glyph_run.run().metrics()), outset by the element’s block padding and border.