Perch
Lua APIFields

Fields

The field schema a widget declares, the typed refs it hands back, conditions and style rules, and the field types. Always granted.

6 min readUpdated Sep 9, 2026

fields is the contract between a widget’s logic and its displays. The widget declares its data with the field.* constructors; the handle exposes the same keys as typed refs on w.fields; every display prop that shows data binds one of those refs; and every present, push and preview is validated against the schema.

--!strict
type fields_t = {
  title: field_ref_t<string>,
  playing: field_ref_t<boolean>,
  faces: list_ref_t<{ name: field_ref_t<string>, avatar: field_ref_t<string> }>,
  chart: record_ref_t<chart_record_t>,
}

local w = widget.new { id = "card", label = "Card", fields = {
  title = field.string(""),
  playing = field.boolean(false),
  faces = field.list { name = field.string(""), avatar = field.image() },
  chart = field.record { points = field.numbers(), up = field.boolean(true) },
} }

Export a fields_t type from displays.luau that names one ref per key, exactly as the schema declares them, and type every display function as function(f: fields_t): node_t. A ref that does not exist is then a diagnostic in Studio, and a nil the builder refuses at load.

The constructors

ConstructorValue pushedRef type
field.string(default?)stringfield_ref_t<string>
field.number(default?)numberfield_ref_t<number>
field.boolean(default?)booleanfield_ref_t<boolean>
field.image()a URL, a file:// path Perch gave you, a data: URI, or ui.asset("x.png")field_ref_t<string>
field.color()a CSS colour value Lua computed (#hex, hsl(), or the monogram gradient form)field_ref_t<string>
field.time_ms()an epoch in milliseconds, for tickersfield_ref_t<number>
field.numbers()an array of numbers: visualizer levels, sparkline pointsfield_ref_t<{ number }>
field.list { ... }an array of records with the element schema givenlist_ref_t<E>
field.record { ... }a nested recordrecord_ref_t<R>

Defaults fill what the first present omits. Records nest to any depth and bind dotted (f.hands.h) or whole (data = f.hands). A list is a top-level field only: field.list inside a record raises field.list() inside a record is not supported; declare the list at the top level.

A provider’s payload is usually wider than your schema. Copy the keys you declared rather than pushing the event table through; an undeclared key raises widget '<id>': unknown field '<key>' (declare it in fields = { … }).

Binding

A data prop takes one of three values:

ValueExampleMeaning
a literaltext = "NOW"fixed
a field reftext = f.titlereads that field of the payload; the node repaints when it changes
a function of the datatext = function(d) return d.n .. " left" endcomputed in Lua once per push of that widget, then bound like a field

That is what bind_t<T> means wherever a prop is declared with it. A record binds whole (points = f.chart) or by sub-ref (src = f.solo.avatar); a list binds only to a repeater’s items, whose item builder receives the element’s refs.

Conditions

Every ref builds conditions. when on a node takes a boolean ref or a condition and gates the node’s presence:

ui.icon { glyph = ui.glyph.pause, when = f.paused }
ui.text { text = "Live", when = f.state:eq("live") }
ui.text { text = "Empty", when = f.rows:is_empty() }
ui.box { when = f.count:gt(3):both(f.enabled) }
MethodOnHolds when
ref:is_true()any refthe value is truthy. ui.when(ref) is the same thing
ref:is_false()any refthe value is falsy (false, "", 0)
ref:eq(v), ref:ne(v)any refthe value equals, or differs from, v
ref:gt(n), ref:lt(n)number refsthe value is above, or below, n
ref:is_empty()string and list refsthe string is "" or the list has no rows
cond:both(other), cond:either(other), cond:negate_()conditionsand, or, not

A gated node starts hidden and shows once its field arrives truthy, so push the gate field along with the rest of the payload.

Style rules

cond:set { ... } turns a condition into a style rule: looks that apply while the condition holds. A node takes up to eight in its style array.

ui.image { src = f.avatar, radius = 999,
  style = {
    f.speaking:is_true():set { ring = { 2, ui.color.live }, glow = { 12, ui.color.live } },
    f.muted:is_true():set { opacity = 0.5 },
  } }

The looks a rule may set are color, bg, opacity, weight, ring, glow, gray and z. Never layout.

Types

export type field_ref_t<T> = {
	is_true: (self: field_ref_t<T>) -> condition_t,
	is_false: (self: field_ref_t<T>) -> condition_t,
	eq: (self: field_ref_t<T>, value: T) -> condition_t,
	ne: (self: field_ref_t<T>, value: T) -> condition_t,
	gt: (self: field_ref_t<T>, value: number) -> condition_t,
	lt: (self: field_ref_t<T>, value: number) -> condition_t,
	is_empty: (self: field_ref_t<T>) -> condition_t,
}

A typed reference to one declared field. gt and lt are for field_ref_t<number>; is_empty for strings and lists.

export type list_ref_t<E> = { item: E, is_empty: (self: list_ref_t<E>) -> condition_t }

What field.list returns. item holds the element’s refs; ui.list { items = f.faces, key = f.faces.item.name } reads them directly.

export type record_ref_t<R> = R & field_ref_t<any>

What field.record returns: the sub-refs plus a ref to the whole record. Spell a record field as record_ref_t<{ h: field_ref_t<number>, m: field_ref_t<number> }>.

export type clock_record_t = { off: field_ref_t<number>, h24: field_ref_t<boolean> }
export type chart_record_t = { points: field_ref_t<{ number }>, up: field_ref_t<boolean> }

The two records components bind whole: ui.ticker in clock mode takes a clock_record_t, ui.sparkline takes a chart_record_t (up picks stroke over stroke_down).

export type bind_t<T> = T | field_ref_t<T> | ((data: any) -> T)

A bindable value: a literal, a ref, or a derived function.

export type condition_t = {
	both: (self: condition_t, other: condition_t) -> condition_t,
	either: (self: condition_t, other: condition_t) -> condition_t,
	negate_: (self: condition_t) -> condition_t,
	set: (self: condition_t, looks: style_looks_t) -> style_rule_t,
}
export type style_rule_t = { __rule: boolean }
export type style_looks_t = { color: color_t?, bg: background_t?, opacity: number?, weight: number?, ring: ring_t?, glow: glow_t?, gray: boolean?, z: number? }

A condition over the widget’s data, the opaque rule set produces, and the looks a rule may flip.

declare field: {
	string: (default: string?) -> field_ref_t<string>,
	number: (default: number?) -> field_ref_t<number>,
	boolean: (default: boolean?) -> field_ref_t<boolean>,
	image: () -> field_ref_t<string>,
	color: () -> field_ref_t<string>,
	time_ms: () -> field_ref_t<number>,
	numbers: () -> field_ref_t<{ number }>,
	list: <E>(element: E) -> list_ref_t<E>,
	record: <R>(shape: R) -> record_ref_t<R>,
}

The constructors, as declared.

esc
Type to search
navigate open