blitz-dom: a containing block laid out as `display: none` keeps its hoisted out-of-flow boxes, and layout panics once one is freed
Generated from repros/blitz-dom-hidden-containing-block-keeps-hoisted/README.md when the site was built at 7aa23e2.
What niso does
Section titled “What niso does”Dom::flush_layout (crates/niso-core/src/lib.rs) runs BaseDocument::resolve under catch_unwind. When it panics,
niso removes every freed node from every node’s hoisted_children and resolves again. The panic cannot be
avoided ahead of time from outside blitz: the pseudo-element case frees the box during box construction, inside the
same resolve whose rounding pass then panics. VS Code for the web hit it about 200 times a run
(docs/compat/vscode-web.md): its .tab-fill is position: absolute, becomes display: none inside
.tabs-container > .tab, and loses a ::before while hidden. Every offsetWidth, offsetHeight and
getBoundingClientRect after that failed with “invalid SlotMap key used”.
Expected vs actual
Section titled “Expected vs actual”taffy’s compute_oof_layout (DioxusLabs/taffy 4aca4f4, src/compute/oof.rs:350-354) records a containing
block’s out-of-flow boxes in hoisted_children, clearing the previous list each time, but returns early, list
untouched, when the output has no oof_positioning_area. A display: none node’s output is
LayoutOutput::HIDDEN, which has none, so blitz’s LayoutPassState::compute_child_layout_in_current_wm
(packages/blitz-dom/src/layout/mod.rs:188-198) leaves the list from the node’s last visible layout in place.
taffy::round_layout still walks it (src/compute/mod.rs:433-437), so a box freed since then is indexed through
LayoutPassState::node_from_id (layout/mod.rs:160) and the slab panics. Two ways to free one:
- blitz itself:
collect_layout_children’s pseudo-element sync (layout/construct.rs:701-706) callsremove_and_drop_pewhen a hidden element’s::before/::afterstops matching. - the embedder:
remove_and_drop_nodeon an absolutely positioned child of a hidden containing block.
::before removed from the hidden containing block (blitz drops the pseudo-element): panic: invalid SlotMap key used expected: no panic actual: panicabsolutely positioned child removed and dropped (remove_and_drop_node): panic: invalid SlotMap key used expected: no panic actual: panicCARGO_TARGET_DIR=~/.capy/work/repro-target cargo runExit status 1 while the bug reproduces, 0 once fixed. On Ubuntu, blitz needs sudo apt-get install -y libfontconfig-dev pkg-config.
Upstream main
Section titled “Upstream main”Same code on main (read 2026-11-08, not built): blitz main still pins taffy 4aca4f4, and its
compute_child_layout_in_current_wm still calls compute_oof_layout only for the node’s own output.
Existing issue or PR
Section titled “Existing issue or PR”None found. Web searches for the panic message with round_layout/hoisted children and for taffy’s hoisted_children with display: none turned up only Bevy’s unrelated taffy-tree SlotMap panics (bevyengine/bevy#12403, #13990, #23415); DioxusLabs/blitz and DioxusLabs/taffy issues were not searched one by one.
Issue text
Section titled “Issue text”Title: A containing block laid out as display: none keeps its stale hoisted_children; round_layout panics once one of them is freed
Body:
compute_oof_layout returns without touching hoisted_children when the output has no oof_positioning_area,
which is the case for LayoutOutput::HIDDEN. A containing block that was laid out visibly, hoisted some
absolutely positioned boxes and is then display: none keeps that list, and round_layout keeps walking it. If one
of those boxes is freed (a ::before that stops matching while the element is hidden, or remove_and_drop_node),
the next resolve panics with “invalid SlotMap key used” in LayoutPassState::node_from_id.
<style>.cb { position: relative } .cb.before::before { content: ""; position: absolute } .hidden { display: none }</style><div class="cb before"><div style="position: absolute">x</div></div>resolve, set the class to cb before hidden, resolve, set it to cb hidden, resolve: panics. Tested at
blitz be60597f67b3cece0456d26e8d7ff5a88600bbc5. Suggested fix: clear the hoisted list when there is no positioning
area (if output.oof_positioning_area.is_none() { tree.clear_hoisted_children(node_id); return; } in
compute_oof_layout), so a hidden or leaf box never keeps the list from an earlier layout.