Skip to content

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.

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”.

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) calls remove_and_drop_pe when a hidden element’s ::before/::after stops matching.
  • the embedder: remove_and_drop_node on 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: panic
absolutely positioned child removed and dropped (remove_and_drop_node):
panic: invalid SlotMap key used
expected: no panic
actual: panic
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.

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.

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.

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.