UI content elements
The leaf elements that display something: UIText, UIImage, UIVideo. Each has one mutable
content property (.text, .src, .player) that lives on the element — content is never a
style. For editable text see UIInput in interactive.md.
At a glance
import photo from './photo.jpg'
const caption = UIText("Loading…").style({ color: "white", fontSize: 16 })
const screen = UIScreen(
UIImage(photo).style({ width: "100%", height: 220, borderRadius: 16, objectFit: "cover" }),
caption,
).style({ bgColor: "black", p: 16, pt: "safe-top", gap: 12 })
screen.open()
caption.text = "Sunset over the harbor" // re-renders immediatelyUIText
UIText(text: string): UIText
text.text = "Updated" // get/set the displayed stringText-specific styles (on top of the common element styles):
textAlign: "start" | "center" | "end" | "left" | "right" // default left
fontFamily: string // system: "serif", "sans-serif", "monospaced"; custom via font() (see fonts.md)
fontSize: UIValue // default 14
lineHeight: UIValue | "normal"
fontWeight: number | "normal" | "bold" // 400, 700, …
fontStyle: "normal" | "italic"
color: Color
textDecoration: "underline" | "line-through" | "none"
letterSpacing: UIValue
lineClamp: number // cap at N lines; 0 / omitted = unbounded
textOverflow: "ellipsis" | "clip" // default ellipsislineHeight: 1.5 means 1.5 px — a bare number is px everywhere. A multiplier is
"1.5em" (resolved against this element's own fontSize; there is no inheritance).
Clamping text to N lines
lineClamp caps the text at N lines, and the element is measured to that height — so the
surrounding layout reflows around the clamped box rather than around the full string:
UIText(article.summary).style({ lineClamp: 3 }) // 3 lines, then "…"
UIText(article.summary).style({ lineClamp: 3, textOverflow: "clip" }) // 3 lines, hard cuttextOverflow only takes effect together with lineClamp — with no clamp the box is sized to
the whole string, so nothing overflows. "clip" is approximate on the web: it cuts on the
line-height box rather than on the glyph, so a descender on the last line may be shaved
differently than on iOS. "ellipsis" is exact on both.
the default screen background is black — always set color explicitly.
UIText's .style() takes no border/background-image props — bgColor, padding, and margins
work, but a decorated text chip is a UIRow/UIColumn around a UIText. Custom fonts must be
registered before the screen opens — see fonts.md.
UIImage
UIImage(src: ImageSource): UIImage
// ImageSource:
// string — remote URL, or an asset() / imported project file
// FetchResponse — a downloaded body used directly as pixels
// File — from openFilePicker()
// SvgSource — wrapped raw SVG XML
// Canvas — a 2D Canvas, baked to a texture on assign
img.src // get the (resolved) source / set a new one — updates the displayed imageImage-specific styles:
objectFit: "cover" | "contain" | "fill" // how the source maps into the box
tintColor: Color // SVG sources only
borderRadius: number // px (number only on UIImage)tintColor applies only to SVG sources, and it replaces all fill and
stroke colors in the SVG — it's for monochrome icons, not multicolored artwork.
a bare relative path string (UIImage("./photo.png")) does not resolve to a bundled
asset — plain strings work only for https://… URLs. Import the file or use asset('./photo.png').
Icons: assetIcon("pack:name")
For icons, prefer the assetIcon() compile macro over hand-writing SVG: it resolves one icon from
the icon registry at build time and inlines it as an image source (the same shape SvgSource
returns), so it renders offline and identically on every host — no network at render time.
UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
UIImage(assetIcon("lucide:check", { color: colors.accent })).style({ width: 20, height: 20 })The "pack:name" id must be a string literal (an unknown id is a compile error; browse packs at
https://icon-registry.jt3.ru). Recolor with the { color } option — a hex literal is baked
into the SVG at compile time, a token/expression is applied as a tint — or with the tintColor
style prop.
Cropping an atlas: setSourceRect
img.setSourceRect(x: number, y: number, w: number, h: number): this // texture pixelsRenders only a sub-rectangle of the source — the spritesheet primitive. The rect's w/h become
the element's intrinsic size (one frame, not the whole atlas), and the cropped frame always fills
the box, overriding objectFit. Chainable and safe to call before the element is on screen (the
initial crop is applied at mount). Sprite-style animation is swapping the rect per frame:
const icon = UIImage(atlasUrl).style({ width: 64, height: 64 }).setSourceRect(0, 0, 128, 128)
let frame = 0
setInterval(() => { icon.setSourceRect((++frame % 8) * 128, 0, 128, 128) }, 100)Pitfall: bgImage is not image content
const img = UIImage("") // ✗ empty source as a placeholder
img.style.bgImage = url // ✗ bgImage is container decoration, not content
const img = UIImage(url) // ✓ the source goes in the constructor…
img.src = newUrl // ✓ …and swaps via .srcUIVideo
UIVideo(player: VideoPlayer): UIVideo
video.player // read-only — the VideoPlayer passed at constructionThe element is just the on-screen surface; playback lives entirely on the
VideoPlayer — create it first, control it directly:
const player = new VideoPlayer(asset('./intro.mp4'))
const video = UIVideo(player).style({ width: "100%", height: 220, borderRadius: 12, objectFit: "cover" })
player.play()Video-specific style:
objectFit: "cover" | "contain" | "fill"UIVideo also accepts the drawable styles (border*, bgColor, …) for framing. The player is
a native resource — dispose() it when the video is gone for good, and stop playback in the
screen's onClose (see Conventions — Lifecycle).
See also
- UI element model — content props vs styles, capturing refs
- Styling — common styles, units,
emresolution - Fonts —
registerFontfor customfontFamily - Media players —
VideoPlayercontrol surface - Interactive elements —
UIInput/UITextAreafor editable text