Primitives
The measurable vocabulary rows are built from.
A row is built from primitives — each one carries both an analytic measure
and a DOM render, so mugen can compute its height and paint it from one
description. A raw <div> has no measure, so it breaks the walk and throws in
development. There are three built-ins — Text, VStack, HStack — plus
definePrimitive(tag) to make your own from any HTML element, and
Escape for content the walker shouldn't measure at all.
Every snippet below is type-checked — hover a primitive or prop to see its type, and the ✗ lines show the actual compiler error.
Text
The only primitive that calls pretext. Its measured height is
layout(prepare(text, font, opts), width, lineHeight); its rendered CSS uses the
identical font, line-height, and spacing, so the two agree. font/lineHeight
fall back to the list defaults on <MugenVList>.
<>{.}</>; // inherits the list font
< ="700 18px Inter" ={24}>{.}</>;
< >{.}</>; // shrink-wrap to natural width| Prop | Type | Notes |
|---|---|---|
children | string | Must be a single string. |
font | Font | Overrides the list default (still measurable). |
lineHeight | number | px. |
letterSpacing | number | px, matches CSS. |
whiteSpace | 'normal' | 'pre-wrap' | pre-wrap keeps \n/tabs/spaces. |
wordBreak | 'normal' | 'keep-all' | |
color | string | |
shrink | boolean | Shrink-wrap to the text's natural width (bubbles). |
VStack / HStack
The two layout boxes. VStack sums child heights plus gaps; HStack takes the
max child height and distributes width the way flexbox paints it — fixed-width
siblings keep their width, the rest take their content width and shrink
proportionally when the row overflows (matching flex: 0 1 auto). A custom
primitive without a naturalWidth makes its row fall back to an equal split
of the remainder.
// avatar | (author over body)
< ={12} ={12}>
< ={40} ={40} ={{ : . }} />
< ={2}>
< ="600 15px Inter">{.}</>
<>{.}</>
</>
</>;Layout is set through props, never CSS, because the props are the chrome the walker counts:
| Prop | Type | |
|---|---|---|
gap | number | Space between children (px). |
padding | number | Uniform padding (px). |
width | number | Declared width — lays out a fixed sibling in an HStack. |
height | number | Declared height — the box's height is this (replaces Fixed). |
align / justify | string | align-items / justify-content. |
A box with a height is a fixed box (an avatar, a spacer); a box with children
derives its height from them.
definePrimitive(tag)
Make a measurable primitive backed by any HTML tag. It's a layout box (like
VStack/HStack, which are themselves definePrimitive('div', …)) with the same
chrome props — plus the tag's own attributes passed straight through, so a
button is clickable and an a takes an href.
const = ('button'); // vertical
const = ('div', { : 'horizontal' });
< ={10} ={() => ()}>
<>{.}</>
</>;This replaces the old Pressable (use a button and its onClick) and Fixed
(use width/height on a box).
Escape
The escape hatch: a fixed-size box that stays in the row's flow at a declared
height (and optional width), but whose children are never walked — so
they can be arbitrary non-primitive React. A shadcn/Radix tooltip (trigger and
all), a chart, an <img>: anything with a known footprint drops in, and any
floating content it portals never touches the row's layout. The walker reads
height without recursing; the render pins the same height inline and clips
overflow, so the children can't desync the row even if they misbehave.
< ={32} ={38} ="flex items-center justify-center">
< /> {/* any React — hooks, raw divs, portals */}
</>;| Prop | Type | Notes |
|---|---|---|
height | number | Required. Border-box height — authoritative for walk and paint. |
width | number | Lays out as a fixed HStack sibling; reported as the natural width. |
style / className | unrestricted | The interior is yours; the frame's height is pinned inline. |
Inside an HStack whose other children wrap, give the Escape a width —
without one its painted width is unknowable, so the row falls back to an equal
split. See Constraints for the
full contract.
Measurability is enforced by types
A primitive's style and className are restricted so its painted box can't
diverge from the box the walker computes. Padding, margin, gap, and sizing are
owned by props — setting them via raw CSS or a utility class is a type error
(hover the ✗ lines):
< ={{ padding: 8 }} />; // ✗ use the `padding` prop< className="p-4 gap-2" />; // ✗ spacing utilities break measurement< ={8} ={2} />; // ✓
< ="rounded-full" />; // ✓ visual-only classes are fineThis is the type-system replacement for the old lint rule: the editor stops you before a height can ever drift.