LeCodesdocs

UI styling

The style system shared by every UI element: one .style() method, one property vocabulary, flexbox-only layout (Yoga engine — no CSS grid, no block flow), logical-px units. This page is the canonical list of style properties and value forms; per-element extras (objectFit, onPressed, scrollDirection, …) live on the element's own page.

At a glance

TypeScript
const card = UIColumn([
  UIText("Title").style({ color: "white", fontSize: 20, fontWeight: 700 }),
])
  .style({ bgColor: "#111", borderRadius: 16, p: 16, gap: 8 })   // merge — chainable
  .style({ onLandscape: { flexDirection: "row" } })              // responsive override

card.style.opacity = 0.5                       // single-prop mutation (hot paths)
card.animateTo({ bgColor: "#333", duration: 200 })   // tween to new values (ms)

Three ways to set styles

TypeScript
el.style({ ... }): this        // MERGES into the current style (does not replace); chainable
el.style.prop = value          // direct single-property write after creation (hot paths)
el.style.prop                  // read back the last value you set

.style() merging means later calls only override the keys they mention:

TypeScript
UIText("Hi").style({ color: "white" }).style({ fontSize: 20 })   // both apply

Use direct mutation for per-frame updates, e.g. el.style.transform = translateY(${y}px)``.

Any primitive-valued prop also accepts a () => value function — a reactive binding that re-applies itself when a signal it read changes:

TypeScript
el.style({ bgColor: () => (selected.value ? "#FF4032" : "#333") })

Animating: animateTo / animateFrom

TypeScript
el.animateTo({ opacity: 0, bgColor: "#000", duration: 300, delay?: 0, commit?: true }): this
el.animateFrom({ opacity: 0, duration: 300, delay?: 0 }): this   // from given values → current
  • duration / delay are milliseconds.
  • animateTo tweens from the current values to the given ones, and commits the targets into the element's style immediately (so reads and later merges see the final state). Pass commit: false to animate without writing the style — e.g. fading out an overlay right before .hide().
  • animateFrom is the entry animation: it snaps to the given values and animates back to the element's current style. It never modifies the stored style.
Note

the options type also allows layer?: number — an escape hatch that targets an internal style layer (the mechanism behind onPressed/orientation styles). Not part of the supported surface.

For free-value tweens (numbers you apply yourself), use animate() — see animate.

Property vocabulary

Layout is flexbox only. Every element is a flex container, position: relative, flexDirection: column by default (UIRow flips it to row).

Container layout (UIRow / UIColumn / UIScrollable / screens / buttons / widgets)

TypeScript
flexDirection:  "row" | "column"
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly"
alignItems:     "flex-start" | "center" | "flex-end" | "stretch"     // default "stretch"
gap:            UIValue
flexWrap:       "nowrap" | "wrap" | "wrap-reverse"

Flex child (all elements)

TypeScript
flex:        number | string
flexGrow:    number            // default 0 — nothing grows without it
flexShrink:  number            // default 0 — nothing shrinks either
flexBase:    UIValue | "auto"  // note: flexBase, not flexBasis
alignSelf:   "flex-start" | "center" | "flex-end" | "stretch"
aspectRatio: number

Size & position (all elements)

TypeScript
width, height:            UIValue | "auto"
minWidth, maxWidth,
minHeight, maxHeight:     UIValue | "auto"
position:                 "relative" | "absolute" | "static"    // default "relative"
top, left, bottom, right: UIValue | safe-area keyword           // offsets; % allowed
boxSizing:                "border-box" | "content-box"          // default "border-box"

Padding & margin (all elements)

Shorthands and long forms are interchangeable (ppadding, ptpaddingTop, …):

TypeScript
p  (padding)                          px (paddingHorizontal)   py (paddingVertical)
pt pb pl pr                           // per side
m  (margin)  — also accepts "auto"    mx (marginHorizontal)    my (marginVertical)
mt mb ml mr  — also accept "auto"

mx: "auto" centers a fixed-width element; a single "auto" margin pushes it to the far side.

Background & border (drawable elements)

Available on containers, UIScreen, UIButton, UIInput, UIVideo. UIText, UIImage, and UISpacer only have bgColor (plus UIImage's own numeric borderRadius).

TypeScript
bgColor:    Color                                       // ≡ backgroundColor; all elements
bgImage:    string | FetchResponse | File               // ≡ backgroundImage; decoration only
bgSize:     "cover" | "contain" | "tile"                // ≡ backgroundSize
bgGradient: string                                      // one+ comma-separated linear-gradient()/radial-gradient() layers

border:       string | number                           // "1px solid #333" or bare width
borderWidth:  number
borderColor:  Color
borderTop / borderRight / borderBottom / borderLeft     // per-side shorthands
borderTopWidth / borderTopColor / …                     // per-side longhands

borderRadius: UIValue | string                          // string = per-corner "0 0 20 20"
borderTopLeftRadius / borderTopRightRadius /
borderBottomLeftRadius / borderBottomRightRadius: UIValue

Background layers paint bottom-to-top: bgColorbgImagebgGradient — so a gradient over an image makes the usual text-protection scrim:

TypeScript
.style({ bgImage: photo, bgSize: "cover",
         bgGradient: "linear-gradient(to top, rgba(0,0,0,0.7), transparent)" })

Gradients (bgGradient)

bgGradient takes a CSS gradient string — linear-gradient() or radial-gradient(). Color stops accept any Color (hex, rgb()/rgba(), named, transparent) with optional % positions.

Linear — an optional direction (to <side> / <angle>deg, default to bottom = 180deg) followed by ≥2 stops:

TypeScript
.style({ bgGradient: "linear-gradient(135deg, #FF4032, #7B2FF7)" })
.style({ bgGradient: "linear-gradient(to right, #000 0%, #333 60%, #fff 100%)" })

Radial — an optional [<shape> || <extent>]? [at <position>]? prefix, then ≥2 stops:

TypeScript
.style({ bgGradient: "radial-gradient(#7B2FF7, #0a0a1a)" })                        // default: ellipse, farthest-corner, at center
.style({ bgGradient: "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)" })     // circle, off-center
.style({ bgGradient: "radial-gradient(ellipse closest-side at top left, #fff, #101014)" })
.style({ bgGradient: "radial-gradient(circle 80px at center, #fff, #101014)" })    // explicit radius

The radial size can be an extent keyword or explicit radii. Both, plus shape and position, are honored on every surface (browser preview, iOS, headless renderer):

Field Values Default
shape circle | ellipse ellipse
extent closest-side | closest-corner | farthest-side | farthest-corner farthest-corner
explicit radii circle <len> or ellipse <len-or-%>{2} — lengths are px/em/vw/calc(), or % of the box axis
at <position> keywords (center, top, left, bottom right, …) or % (20% 80%) center
Radial fidelity

the browser preview renders every CSS radial form exactly (it's raw CSS). On device (iOS) and in the headless renderer the fields above are resolved 1:1. Rarer CSS forms outside this table (e.g. 4-value at left 10% top 20% positions) fall back to a centered default there while the preview stays exact. conic-gradient() is not supported.

Layering multiple gradients. Comma-separate several gradients (like CSS background-image) to stack them — the first listed paints on top, so lead with the ones that have transparent regions:

TypeScript
.style({ bgColor: "#0a0a1a", bgGradient:
  "radial-gradient(circle at 15% 20%, #7B2FF7, transparent 60%), " +
  "radial-gradient(circle at 85% 80%, #2FB0F7, transparent 55%)" })

Linear and radial layers can be mixed, each honors the full syntax above, and the stack is composited on every surface (browser preview, iOS, headless renderer) — a common way to build soft multi-glow or "aurora" backgrounds.

Note

bgImage is decoration behind children. An image that is the content belongs in UIImage — see content.md.

Everything else (all elements)

TypeScript
opacity:       number | string          // 0..1
transform:     string                   // CSS-style string, e.g. `translateY(12px)` — great for gestures
display:       "none" | "flex"          // "none" removes it from layout
overflow:      "visible" | "hidden"     // default "hidden" — children are clipped
pointerEvents: "all" | "none"           // drawable elements; "none" lets taps pass through

Values & units (UIValue)

TypeScript
16                  // bare number = logical px (the default unit everywhere)
"50%"               // percent of the parent
"50vw" "50vh"       // viewport width/height
"50vmin" "50vmax"
"1.5em"             // × the element's OWN fontSize (default 14) — there is NO style inheritance
"calc(100vw - 32px)"
"min(...)" "max(...)" "clamp(min, val, max)"
  • em resolves against the element's own fontSize, never a parent's — set fontSize on the same element or em means 14px-relative.
  • calc() / min() / max() / clamp() combine px, viewport units, em, and safe-area keywords, and nest freely — but do not support %.
TypeScript
width: "calc(100% - 20px)"   // ✗ % can't appear inside calc
width: "calc(100vw - 20px)"  // ✓ use viewport units instead

Colors (UI styles only)

UI color strings are parsed by the UI engine and accept more than the engine-side ColorInput (conventions):

TypeScript
"#f33" "#f33c" "#ff3333" "#ff3333cc"      // hex, 3/4/6/8 digits
"rgb(255, 51, 51)" "rgba(255, 51, 51, 0.8)"
0xff3333                                   // packed int
// named — exactly this set, nothing more:
"white" "black" "red" "green" "blue" "yellow" "orange" "purple"
"gray" "cyan" "magenta" "brown" "transparent" "clear"
Note

this parser exists only for UI styles. 2D/3D APIs (Sprite.color, Material, Scene2D background) accept hex strings and packed ints only — rgba(...) and named colors silently become opaque black there.

Safe areas

Keyword values that resolve to the device's safe insets (notch, home indicator). The exact set:

TypeScript
safe-top      safe-bottom      safe-left      safe-right
safe-top-comfort  safe-bottom-comfort  safe-left-comfort  safe-right-comfort
safe-all      safe-all-comfort          // only on the p / m (padding / margin) shorthands

Where they're accepted:

  • padding per-side (pt, paddingBottom, …) — the side-matching keyword; p/padding takes safe-all
  • margin per-side and m/margin — same set (alongside "auto")
  • position offsets top / left / bottom / right — the side-matching keyword
  • inside calc()/min()/max() — the canonical header pattern:
TypeScript
.style({ pt: "max(safe-top, 24px)" })   // at least 24px even on inset-less devices

The -comfort variants never collapse to zero: they guarantee comfortable breathing room (≥ 12 logical px vertically by default) even when the raw inset is 0 — use them for content that would otherwise touch the screen edge on old devices.

Responsive: onLandscape / onPortait

Any style object can nest orientation overrides; they merge on top when the device is in that orientation and lift off when it leaves:

TypeScript
screen.style({ flexDirection: "column", p: 16, onLandscape: { flexDirection: "row", p: 32 } })
Note

the portrait key really is spelled onPortait (missing the second "r") — that is the actual API name.

Style classes

A style class is a named style state you declare inline and toggle from code — like onPressed, but with any name and driven by you, not the system. Use it for persistent states: selected, checked, active, expanded.

Declare a class with a $-prefixed key inside .style(); its block holds the overrides (plus an optional transition duration / delay). Activate or deactivate it on the element:

TypeScript
const toggle = UIButton([ UIText("Dark mode") ]).style({
  bgColor: "#222",
  $checked: { bgColor: "#FF4032", duration: 150 },   // declared like onPressed
})

toggle.onClick(() => toggle.toggleClass("checked"))   // flip it
// or set explicitly:
toggle.setClass("checked", true)
toggle.hasClass("checked")   // → true
TypeScript
el.setClass(name, enabled): this   // activate (true) / deactivate (false) a class on this element
el.toggleClass(name): this         // flip it
el.hasClass(name): boolean         // is it currently active on this element?
  • The name is given with or without the leading $setClass("checked", …) and setClass("$checked", …) are equivalent.
  • Activation is per element: it restyles only the element you call it on, not its children.
  • The active set is remembered on the element, so a class stays active across a screen being closed and re-opened.
  • The transition (duration / delay, ms) makes the swap animate, on every platform.

Precedence — when two states set the same prop, the later-declared and more-interactive one wins, in this order (lowest → highest):

TypeScript
base style  <  $classes (later-declared beats earlier)  <  onPressed / onFocused

So a class beats the base style, a class declared later beats one declared earlier, and onPressed (or onFocused) always beats a class while the element is pressed/focused. Example — a checked button that still shows press feedback:

TypeScript
UIButton([ UIText("Save") ]).style({
  bgColor: "#222",
  $checked: { bgColor: "#2a7" },        // shown when checked
  onPressed: { bgColor: "#195" },       // wins while the finger is down, even when checked
})
Reactive shortcut

to bind a class to a signal instead of toggling it by hand, a primitive style prop already accepts a () => value binding (see above) — reach for a class when you want a named, reusable state block rather than a single computed value.

Defaults that surprise

  • flexShrink: 0 — elements don't shrink to fit; a UIScrollable in a column overflows instead of scrolling until it (and any wrapping ancestors) get flexShrink: 1.
  • flexGrow: 0 — nothing grows along the main axis without asking; there are no implicit min-sizes either.
  • alignItems: "stretch" — children fill the cross axis by default; set a size or alignSelf to opt out.
  • overflow: "hidden" — children are clipped to the parent's box.
  • position: "relative", boxSizing: "border-box" (width/height include padding + border).
  • The screen background is black and default fontSize is 14 — always set bgColor and text color explicitly.

Logical px & the design canvas

All units are logical px (iOS pt / Android dp), never physical pixels — fontSize: 16 looks the same on every device. Design for a phone canvas of width ~360–430 (390 is a good default) and height ~670–930; vertical space is scarce on SE-class screens, so long content belongs in a UIScrollable or a scrollable screen.

Pitfalls

TypeScript
display: "grid"                    // ✗ flexbox only (Yoga)
lineHeight: 1.5                    // ✗ a number is px (= 1.5px); a multiplier is "1.5em"
node.style({ width: "calc(100% - 20px)" })   // ✗ no % inside calc — use 100vw

See also