When hovering makes an element flicker
An interaction can move the boundary that detects it. Compare the visible size, reserved layout space, and pointer region before adding delays to a flickering hover effect.
Read MarkdownUpdated 2026-09-17 Try it: problem and fix
An interactive illustration of the mechanism. Sample data and timing are for learning, not a replay or benchmark of the source application.
Enable JavaScript to run the example. The explanation and verification steps below also work without it.
When to use this lesson
- An element expands and collapses repeatedly while the pointer is still.
- Hovering a card, menu, or node moves the target away from the cursor.
- An expanded item overlaps its neighbors or jumps position.
What happened
In MuxMap, an expanded graph card flickered. Hover increased its rendered size while the layout still reserved the compact size; selected cards and hovered cards used different expansion rules.
Diagnosis and approach
The patch exposes a disagreement between layout geometry and visual geometry. Changing size could move or overlap the interactive region, making raw enter/leave events unreliable at the boundary.
To diagnose a similar effect, compare the bounding rectangle before and after expansion, log which event clears hover, and trace the predicates used by layout and rendering. These are checks to perform, not a recorded measurement from this incident.
What did not work
A 150 ms leave grace period already existed before the fix. It did not make layout reserve the hovered card’s larger dimensions. The patch retained that delay while correcting the geometry and checking pointer containment. Increasing the timeout alone would leave the mismatch; the evidence does not record an experiment with a longer timeout.
What changed
Use one expansion predicate for both layout dimensions and rendering. In MuxMap, selection or hover causes expansion. Calculate width and height once, then render those dimensions.
Before clearing hover, check the pointer against the expanded element rectangle. A modest spatial margin and a short leave grace period stabilize boundary crossings; cancel the pending clear when the pointer returns. The recorded implementation used a 40 px margin and 150 ms grace period. These values are project choices, not universal constants.
If the effect is purely decorative, a stable outer hit area with a transformed inner visual may avoid layout changes entirely. That is an alternative to evaluate, not the implementation of this commit.
How to verify the improvement
- Enter the node and stop moving the pointer. Observe several frames: expansion must not toggle.
- Move slowly across each edge, including the expanded area and grace margin.
- Check nearby nodes at different zoom levels and confirm the layout reserves the rendered dimensions.
- Confirm leaving the intended region eventually collapses an unselected node.
- Inspect intermediate states and bounding rectangles; a screenshot taken after settling can miss the flicker.
What to do next time
When hover changes size, use a consistent expansion rule for layout, rendering, and pointer containment. A stationary pointer inside the intended interaction region should produce one stable expansion. Use a small margin or delay only for a real boundary crossing. Verify intermediate frames because a settled screenshot can hide flicker.
Limits and exceptions
Applies when hover changes component geometry. A grace period should address a real boundary crossing, not hide conflicting state ownership. Persistent selection may intentionally keep a node expanded after hover ends. The particular margin and delay need to match the interaction.
Commit evidence
- syntaxskills/muxmap@c056323: Unified expanded layout and render dimensions, added pointer containment against an inflated rectangle, and retained a bounded leave grace period.
Based on the linked commits. Verification describes checks to perform, not a claim that tests were executed. Confirm applicability in your own project.