mugen

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; use useMugenEffect,
  • 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 propsgap, 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 listsText-dominant, structurally-regular rows.
❌ Horizontal / gridspretext is height-from-width; the inverted axis is a separate design.
❌ Arbitrary CSS rowsOnly the measurable primitive vocabulary can be walked.
⚠️ Animated heightmugen 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.

On this page