Your first screen
Step 1 of the UI track. We start the track's project — a small habit tracker — with its home screen: a header, a row of stats, a list of habit cards and a button. No state or interactivity yet (that's the next step); today is layout and styles.
Every example on this page is a complete program: the phone next to you shows exactly the code you're reading, and any example can be pasted into a fresh project as-is.
The skeleton
Every UI app starts the same way: a UIScreen, children inside, .open() at the end.
const screen = UIScreen([
UIText("Today").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })
screen.open()What to notice before we build on it:
- A screen always fills the device — you don't size it, you style its padding, background and how children lay out.
pt: "max(safe-top, 24px)"keeps the header clear of the notch on real phones and still gives 24 px in the browser. - A screen behaves as a column: children stack top to bottom,
gapspaces them. - The subtitle isn't a string — it's a call to the SDK's date():
format("dddd, DD MMMM")builds today's date in the device's language, English or Russian. That's why the phone next to you shows the real "today". - All sizes are logical px (like CSS px / iOS pt), and
.style()merges — a later call only overrides the keys it mentions. - The default background is black and text has no default color — always set
bgColorand text colors explicitly.
Rows inside columns
The stats strip is the classic flexbox move: a UIRow of three cards, each a small UIColumn. Two defaults matter here and differ from the web: nothing grows or shrinks unless you say so (flexGrow / flexShrink default to 0), and alignItems defaults to stretch.
const screen = UIScreen([
UIText("Today").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),
UIRow([
UIColumn([
UIText("3").style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText("done").style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" }),
UIColumn([
UIText("2").style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText("left").style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" }),
UIColumn([
UIText("71%").style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText("this week").style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" }),
]).style({ gap: 10, mt: 8 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })
screen.open()flex: 1 on each card is what makes the three of them share the row's width equally. flex: 1 is flexGrow: 1 together with flexBasis: 0: each card starts from zero base width, so they split the row into exact thirds instead of growing from different content-based widths (flexGrow: 1 alone isn't enough — the "71%" card would stay a bit wider). Drop it and the cards hug their text instead. And yes — three copies of the same card is painful to read. That's deliberate: it's the next section's job.
Compose with functions
Three near-identical cards beg for a function — and that's the whole component model of LeCodes UI: elements are plain objects, so a "component" is just a function that returns one. No JSX, no registration, nothing to learn:
const Stat = (value: string, label: string) =>
UIColumn([
UIText(value).style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText(label).style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" })
const screen = UIScreen([
UIText("Today").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),
UIRow([
Stat("3", "done"),
Stat("2", "left"),
Stat("71%", "this week")
]).style({ gap: 10, mt: 8 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })
screen.open()The phone shows the same screen as before — that's the point. Same pixels, a third of the code.
The habit card is the same idea with a bit more layout — a row of [check circle | title+time], where flexGrow: 1 on the text column pushes everything else aside. Here it runs on its own, a list of three:
const Habit = (title: string, time: string, color: string, done: boolean) =>
UIRow([
UIColumn([
UIText(done ? "✓" : "").style({ color: "white", fontSize: 15, fontWeight: 700 }),
]).style({
width: 28, height: 28, borderRadius: 14,
bgColor: done ? color : "transparent",
border: done ? "0" : `2px solid ${color}`,
alignItems: "center", justifyContent: "center",
}),
UIColumn([
UIText(title).style({ color: "white", fontSize: 16, fontWeight: 600 }),
UIText(time).style({ color: "#8a8a93", fontSize: 13 }),
]).style({ gap: 2, flexGrow: 1 }),
]).style({ bgColor: "#1a1a20", borderRadius: 16, p: 14, gap: 12, alignItems: "center", opacity: done ? 0.75 : 1 })
const screen = UIScreen([
UIColumn([
Habit("Morning run", "07:30", "#34c759", true),
Habit("Read 20 pages", "13:00", "#ff9f0a", true),
Habit("Stretch", "20:30", "#af6bff", false),
]).style({ gap: 10 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)" })
screen.open()The check circle is a fixed 28×28 UIColumn with borderRadius at half its size — there is no special "circle" element, and centering its glyph is the usual alignItems: "center", justifyContent: "center".
Pinning the button to the bottom
The last trick: UISpacer() between the list and the button. A spacer is an empty element with flexGrow: 1 — it eats all the free vertical space, pushing the button to the bottom of the screen no matter how many cards there are.
UISpacer(),
UIButton([
UIText("+ New habit").style({ color: "white", fontSize: 16, fontWeight: 700 }),
]).style({ bgColor: "#FF4032", borderRadius: 16, p: 16, alignItems: "center" }),The button doesn't do anything yet — wiring it up is exactly where the next step picks up.
The whole screen
Everything from this page assembled, top to bottom — the app from the intro:
const ACCENT = "#FF4032"
const BG = "#0e0e12"
const CARD = "#1a1a20"
const MUTED = "#8a8a93"
const Stat = (value: string, label: string) =>
UIColumn([
UIText(value).style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText(label).style({ color: MUTED, fontSize: 13 }),
]).style({ bgColor: CARD, borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" })
const Habit = (title: string, time: string, color: string, done: boolean) =>
UIRow([
UIColumn([
UIText(done ? "✓" : "").style({ color: "white", fontSize: 15, fontWeight: 700 }),
]).style({
width: 28, height: 28, borderRadius: 14,
bgColor: done ? color : "transparent",
border: done ? "0" : `2px solid ${color}`,
alignItems: "center", justifyContent: "center",
}),
UIColumn([
UIText(title).style({ color: "white", fontSize: 16, fontWeight: 600 }),
UIText(time).style({ color: MUTED, fontSize: 13 }),
]).style({ gap: 2, flexGrow: 1 }),
]).style({ bgColor: CARD, borderRadius: 16, p: 14, gap: 12, alignItems: "center", opacity: done ? 0.75 : 1 })
const screen = UIScreen([
UIText("Today").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: MUTED, fontSize: 15 }),
UIRow([
Stat("3", "done"),
Stat("2", "left"),
Stat("71%", "this week"),
]).style({ gap: 10, mt: 8 }),
UIColumn([
Habit("Morning run", "07:30", "#34c759", true),
Habit("Read 20 pages", "13:00", "#ff9f0a", true),
Habit("Practice piano", "18:00", "#5e9eff", true),
Habit("Stretch", "20:30", "#af6bff", false),
Habit("Plan tomorrow", "22:00", ACCENT, false),
]).style({ gap: 10, mt: 12 }),
UISpacer(),
UIButton([
UIText("+ New habit").style({ color: "white", fontSize: 16, fontWeight: 700 }),
]).style({ bgColor: ACCENT, borderRadius: 16, p: 16, alignItems: "center" }),
]).style({ bgColor: BG, p: 20, pt: "max(safe-top, 24px)", pb: "max(safe-bottom, 20px)", gap: 4 })
screen.open()What you learned
UIScreenfills the device; style its padding (safe-top/safe-bottom), background andgap— not its size.- Layout is flexbox: columns by default,
UIRowfor rows,flex: 1to split space equally (flexBasis: 0),flexGrow: 1to grab the leftover,UISpacer()to push things apart. - A component is a plain function returning an element — composition needs no framework.
.style()merges; sizes are logical px; set colors explicitly.
Reference for everything used here: Element model · Styling · Containers · Text, images, video · Buttons · Dates.
Next: State & reactivity — the button starts adding habits and the stats start counting. (being written)