Constraints
The invariants that make analytic measurement possible — and v1 limits.
mugen trades flexibility for exactness. These rules are what let it measure a row without mounting it. Most are enforced — by the type system, or in development with a precise error.
Rows are pure item → tree
mugen runs render(item) to measure, calling it as a plain function. Therefore a
row may not:
- use
useEffect/useLayoutEffect— they throw; useuseMugenEffect, - measure a child via a ref,
- contain content whose height the walker can't derive (unmeasurable content goes in an
Escape, which has a declared height), - depend on anything but its item and the mugen hooks for its height.
Cosmetic useState is allowed (it can't change height). Height-affecting state —
the kind that does — goes through useMugenState. The mugen hooks must be
called directly in render (not a nested JSX-element component) and in the same
order every render.
Only primitives are measurable
A row may contain primitives (Text, VStack, HStack, or
your own definePrimitive(tag)) and hook-free components composed from them. A raw
<div> has no measure and breaks the walk — so a row's render may return:
<>{.}</>; // ❌ runtime: not measurable
<><>{.}</></>; // ✅Raw strings must be wrapped in <Text> so they can be measured. Fragments
(<>…</>) are fine — they paint no box, so the walker splices their children in
place and measures them as ordinary siblings.
Escaping the walker: Escape
The sanctioned exception to "only primitives are measurable" is Escape — a
fixed-size box that stays in the row's flow at a declared height (and
optional width) but whose children are never walked. Inside it, anything
goes: hooks, raw <div>s, a stock shadcn/ui or Radix
Tooltip, a chart, an <img>. The contract is foreignObject's — mugen reserves
exactly the box you declare, and you design the children within it. The render
pins the same height inline and clips overflow, so the painted row can't desync
from the computed one even if the children misbehave.
< ={10} ="center">
<>{.}</> {/* measured normally — wraps, drives the row height */}
< ={32} ={38}>
< ={.} /> {/* any React — never walked */}
</>
</>;This is also the overlay story: a complete shadcn Tooltip / Popover /
DropdownMenu / Dialog drops inside an Escape — trigger included — because
those libraries portal their floating panels to document.body, where mugen's
layout never sees them. Use Escape for anything with a known footprint
(icon buttons, avatars, badges, embeds, toolbars); content whose height must
come from the text itself — wrapping paragraphs, markdown — still belongs to
measured primitives. In an HStack whose other children wrap, give the
Escape a width so the row distributes width exactly.
The older escape hatch, Portal (an out-of-flow subtree measured as 0), is
deprecated: overlay libraries portal their own content, so a separate
measured-as-0 half is no longer needed. It keeps working, but new code should
reach for Escape.
Layout goes through props, not CSS
Padding, margin, gap, and sizing are the chrome the walker counts, so they're
props — gap, padding, width, height. Setting them through style or a
utility className is a type error, because the painted box would diverge from
the measured one. Visual-only styles (color, background, border-radius) are fine.
Fonts must match the CSS
Whatever font / lineHeight / letterSpacing a Text measures with must equal
the CSS that paints it. A Text carries its own (or inherits the list defaults),
and the same values feed measurement and render — so they can't drift. system-ui
is rejected outright.
Keep row bodies cheap
Re-measuring re-runs a row's body. Expensive per-row derivation belongs in
useMugenMemo (recomputed only on deps change); keep the body a
thin map from data to primitives.
v1 scope
| ✅ Vertical lists | Text-dominant, structurally-regular rows. |
| ❌ Horizontal / grids | pretext is height-from-width; the inverted axis is a separate design. |
| ❌ Arbitrary CSS rows | Only the measurable primitive vocabulary can be walked. |
| ⚠️ Animated height | mugen commits the final height and lets you animate visually. Exact per-frame offsets would mean re-patching every frame. |
The fit test
mugen pays off when a row's height is text (measured by pretext) + declared
chrome. Fixed-footprint extras — charts, embeds, widgets of known size — ride
along in an Escape. But if your rows are
dominated by content of genuinely unknown height (media of unknown aspect,
arbitrary CSS), you've left the regime where analytic measurement wins, and a
DOM-measuring virtualizer may fit better. Before committing, try rebuilding
your ugliest real row in primitives plus declared Escape boxes.