LeCodesdocs

UI element model

How the UI layer works as a whole: elements are plain objects built by global factory functions, composed through children arrays, configured by chaining, and updated by mutating properties — no JSX, no virtual DOM, no re-render cycle. Layout is flexbox (Yoga) in logical px, screen space Y-down. This page covers the model; the rest of the section covers the elements:

At a glance

TypeScript
let counter = 0
let label: UIText

const screen = UIScreen([
  label = UIText("Taps: 0").style({ color: "white", fontSize: 24, fontWeight: 700 }),
  counter > 0 ? UIText("already tapped") : null,        // null children are skipped
  UIButton([ UIText("Tap").style({ color: "white" }) ])
    .style({ bgColor: "#FF4032", borderRadius: 12, p: 16, alignSelf: "flex-start" })
    .onClick(() => { label.text = `Taps: ${++counter}` }),   // mutate content directly
]).style({ bgColor: "black", p: 20, pt: "max(safe-top, 24px)", gap: 16 })

screen.open()

Factories, not constructors

Every element is created by calling a global function — never new:

TypeScript
UIColumn(children?)              // containers: an optional children array
UIText(text)                     // content elements: the content itself
UIImage(src)

The factory takes only the element's content; everything else is configured by chaining — .style(), .onClick(), .append(), .animateTo() — each returns the element itself, so construction reads as one chain.

Legacy

every factory also accepts a style object as an optional first argument (UIText({ fontSize: 20 }, "Hi")). It still works and appears in older projects, but new code sets styles through .style().

Elements carry a readonly type string ("column", "text", …) identifying what they are.

Children arrays & conditional rendering

Containers take a plain array. A null, undefined, or false entry is skipped entirely — no element, no layout slot — which is the conditional-rendering idiom:

TypeScript
UIColumn([
  header,
  isLoading ? spinner : null,     // ternary with null
  showFooter && footer,           // && short-circuit
])
TypeScript
// ✗ empty container as a placeholder (web habit) — it still occupies a flex slot
UIRow([ isGroup ? button : UIColumn([]) ])
// ✓ null is skipped — no phantom element
UIRow([ isGroup ? button : null ])

Capturing references

To keep a handle on a nested element, use an assignment expression inside the array — it both sets the variable and adds the element:

TypeScript
let label: UIText
let input: UIInput

UIColumn([
  label = UIText("Hello"),
  input = UIInput(),
])

label.text = "Updated"       // later
console.log(input.value)

let is a statement, not an expression — declare outside, assign inside:

TypeScript
UIRow([ let input = UIInput() ])   // ✗ syntax error

Prefer captured refs over indexing container.childrenchildren entries are untyped (UINodeChild), so reading them back requires a cast.

Content properties vs styles

Mutable content lives directly on the element, not in the style. Setting it re-renders immediately, mounted or not:

TypeScript
text.text = "Updated"        // UIText
input.value = ""             // UIInput / UITextArea
image.src = newUrl           // UIImage
video.player                 // UIVideo — read-only link to its VideoPlayer

Styles carry appearance and layout only. In particular, never put an image's content in bgImage — that's a container background. See content.md.

Updating children imperatively

Containers (UIRow, UIColumn, UIScrollable, …) expose direct child manipulation — there is no diffing; you state the change:

TypeScript
list.setContent(items.map(Row))       // replace all children
list.append(row1, row2)               // add at the end
list.insert(0, banner)                // add at an index (into .children)
list.remove(row1)                     // remove by identity
list.children                         // the current array (readonly)

All four work both before and after the element is on screen. For long or unbounded data, use UIVirtualizedList instead of setContent over a big array.

Reading measured size — onLayout

Sizes exist only after layout. There is no synchronous getter (no el.width, no getBoundingClientRect) — onLayout is the only way to read an element's box:

TypeScript
el.onLayout(({ left, top, width, height }) => { ... })   // logical px; top/left relative to the parent

It fires when the element first gets a layout and again whenever its box changes (resize, content change). Multiple callbacks can be registered; each call chains.

Reusable components

A "component" is just a factory function returning an element — chain onto the result like any other element. Shared styles are plain objects typed with the global Style<T> helper (a type only — nothing to import or instantiate):

TypeScript
const heading: Style<UIText> = { color: "white", fontSize: 24, fontWeight: 700 }

const Card = (title: string, subtitle: string) => UIButton([
  UIText(title).style({ fontWeight: 700, color: "white" }),
  UIText(subtitle).style({ color: "#888", fontSize: 13 }),
]).style({ px: 16, py: 12, gap: 4, flexDirection: "column", alignItems: "flex-start" })

Card("Title", "Subtitle").style({ bgColor: "#111" }).onClick(() => { ... })
Note

every element accepts a name string in its style object — a semantic label, not a style. It's extracted at construction and surfaced on the node (el.name) as a stable selector for tests and review tooling.

See also