Fonts
font() gives text a custom font: a compile-time macro that installs a family — from the built-in
registry or a .ttf / .otf of the project — and returns its name for fontFamily; nothing to
load or await. registerFont loads a face from a URL known only at runtime.
- use a ready-made font such as Inter, Manrope or Playfair Display (
font("inter")) - use your own font file from the project (
font("./fonts/Brand.ttf")) - set one font for the whole app, or for one part of the screen
- use a second font for headings or numbers
- make text in a custom font bold, semi-bold or italic
- show text in the system serif or monospace font
- load a font from a URL at runtime (
registerFont)
At a glance
theme({ fontFamily: font("manrope") }) // the font of every text that names none
UIScreen(
UIText("Weekly report").style({ fontFamily: font("unbounded"), fontSize: 28, color: "#1C1C1E" }),
UIText("Body text in Manrope, the app's font").style({ fontSize: 16, color: "#1C1C1E" }),
UIText("Bold Manrope").style({ fontSize: 16, fontWeight: 700, color: "#1C1C1E" }),
).style({ bgColor: "#FFFFFF", p: 16, gap: 8 }).open() // nothing to await before itUse a custom font from the registry: font("id")
To give text a ready-made custom font, pass its registry id to font(): the call installs the
family and compiles to its name — font("manrope") is the string "Manrope" — which goes into
fontFamily. The faces are registered before your code runs.
font(id: string, opts?: { weights?: number[], italic?: boolean }): string- The argument is a string literal. A variable, an unknown id (the error lists ids) or
font("serif")(see System serif and monospace fonts) is a compile error. - Every family covers Latin and Cyrillic but
space-grotesk(Latin only); all are OFL-licensed. fontis a macro only where it is the global: a file that declares or imports its ownfont(const font = { h2: … }, say) is skipped — everyfont("…")in it stays a plain call.
| category | ids |
|---|---|
| sans-serif | inter manrope golos-text onest rubik nunito montserrat ibm-plex-sans pt-sans jost exo-2 |
| display | unbounded russo-one oswald comfortaa space-grotesk |
| serif | playfair-display lora pt-serif cormorant literata |
| monospace | jetbrains-mono fira-code ibm-plex-mono |
| handwriting | caveat pacifico |
Bold, semi-bold and italic faces: weights and italic
Text in a weight needs that weight's face installed. font("id") installs the family's default
weights: 400 / 500 / 700 for most sans families (Nunito 400 / 600 / 700, PT Sans 400 / 700), 400 /
700 for the others (Russo One and Pacifico ship 400 only). { weights } replaces that list;
{ italic: true } adds an italic face for every weight in it.
const ui = font("inter", { weights: [400, 600, 800] }) // these three faces, no 500 or 700
const quote = font("lora", { italic: true }) // 400 and 700, upright and italic
UIScreen(
UIText("Semi-bold title").style({ fontFamily: ui, fontWeight: 600, fontSize: 22, color: "#1C1C1E" }),
UIText("1,240").style({ fontFamily: ui, fontWeight: 800, fontSize: 34, color: "#1C1C1E" }),
UIText("An italic quote").style({ fontFamily: quote, fontStyle: "italic", fontSize: 18, color: "#1C1C1E" }),
).style({ bgColor: "#FFFFFF", p: 16, gap: 8 }).open()- A weight the family doesn't ship is a compile error that lists the ones it does.
italicneeds an italic for every weight of the list:font("nunito", { italic: true })fails on Nunito's 600 — narrow it ({ italic: true, weights: [400, 700] }). Italics exist for every weight ofrubikand for 400 / 700 ofnunitomontserratibm-plex-sanspt-sansjostexo-2playfair-displaylorapt-serifcormorantliteratajetbrains-monoibm-plex-mono; the other families have none (a compile error says so).
install every weight you style text with. On iOS and Android a fontWeight with no
installed face is drawn in the system font, not the family; desktop uses the family's regular
face, a browser the nearest one.
Use your own font file: font("./fonts/Brand.ttf")
To use a font file that ships with the project, pass its path to font(). The compiler reads the
family name from the file's name table and the weight and italic from its OS/2 table, so there
is nothing to name and no option to pass.
// fonts/Brand-Regular.ttf and fonts/Brand-Bold.ttf sit next to main.ts
const brand = font("./fonts/Brand-Regular.ttf") // the family name read from the file
font("./fonts/Brand-Bold.ttf") // the same family's bold face
UIScreen(
UIText("Brand headline").style({ fontFamily: brand, fontWeight: 700, fontSize: 28, color: "#1C1C1E" }),
UIText("Brand body text").style({ fontFamily: brand, fontSize: 16, color: "#1C1C1E" }),
).style({ bgColor: "#FFFFFF", p: 16, gap: 8 }).open()- One file is one face: call
font()once per file (regular, bold, italic); the calls of one family add up. .ttf/.otfonly: WOFF / WOFF2 and collections (.ttc) are compile errors — convert, or export single faces. Options are a compile error too.- The path starts with
./or../and is relative to the calling file, likeasset(); a file that isn't there is a compile error.
Set the app's font, and another for headings: theme({ fontFamily })
To give the whole app one font, set the theme's fontFamily: every UIText and UIInput that
names no fontFamily of its own uses it. A second family — headings, numbers — goes on the texts
that need it, best as a shared Style<UIText>. A part of the screen gets its own default with
el.theme({ fontFamily }) (theme.md); a
container's style has no fontFamily.
theme({ fontFamily: font("inter") }) // the whole app
const heading: Style<UIText> = { fontFamily: font("playfair-display"), fontSize: 26, color: "#1C1C1E" }
const code = UIColumn(
UIText("npm install lecodes-sdk").style({ fontSize: 14, color: "#1C1C1E" }),
).style({ p: 12, bgColor: "#F2F2F7", borderRadius: 8 })
code.theme({ fontFamily: font("jetbrains-mono") }) // the texts in this card only
UIScreen(
UIText("Getting started").style(heading),
UIText("Body text in Inter").style({ fontSize: 16, color: "#1C1C1E" }),
code,
).style({ bgColor: "#FFFFFF", p: 16, gap: 12 }).open()Without a theme font, text is in the host's default: the system font on iOS and Android, Inter on
desktop, Roboto on the web (app.json "web": { "font": "system" } uses the device's font there
instead) — set one for the same look everywhere.
System serif and monospace fonts
The system's own families are plain fontFamily strings — "serif", "sans-serif",
"monospaced" (CSS's "monospace" works too) — with nothing installed or fetched:
UIScreen(
UIText("A quote in the platform's serif").style({ fontFamily: "serif", fontSize: 18, color: "#1C1C1E" }),
UIText("SKU-10442").style({ fontFamily: "monospaced", fontSize: 14, color: "#1C1C1E" }),
UIText("The system sans").style({ fontFamily: "sans-serif", fontSize: 14, color: "#1C1C1E" }),
).style({ bgColor: "#FFFFFF", p: 16, gap: 8 }).open()Each host draws them in its own face — iOS New York and SF Mono, Android Typeface.SERIF /
MONOSPACE, desktop Times New Roman and Consolas on Windows (Liberation, DejaVu or Noto on
Linux), a browser its CSS generics. Use them for the platform's look and a registry font for the
same face everywhere. Headless renders (lecodes render, lecodes test) pin them to bundled
faces so output is the same on any machine: serif → PT Serif, monospaced → JetBrains Mono,
sans-serif → Roboto.
How fonts load — nothing to await
Faces declared with font() are known at compile time, so no code waits for them:
- A build that carries its assets — a local project (
lecodes init),desktop run,render,web build, a native shell — has the faces in the bundle. The CLI downloads registry faces into the project once:assets/fonts/,fonts.lock.jsonandFONT-NOTICES.md(the licenses) appear next to your code. iOS, Android and desktop register them before the first layout. - A project built by le.codes keeps registry faces on the font CDN: iOS, Android and desktop fetch them as the app starts and draw text in the default font until a face lands, then lay the text out again in it.
- On the web (the web build and the web player) the page is held until the declared faces are in, at most 1.5–2 s; a face that comes later swaps in when it lands.
Load a font from a URL at runtime: registerFont
For a font whose URL is known only while the app runs — a font picker, a user's own font — call
registerFont(family, url) once per face (weight + style), then name the family in fontFamily.
Text that names it switches to the face when it lands; nothing has to wait.
registerFont(fontFamily: string, url: string, options?: { weight?: number, style?: "normal" | "italic" }): Promise<void>const sample = UIText("The quick brown fox").style({ fontSize: 22, color: "#1C1C1E" })
const useFont = (family: string, url: string) => {
registerFont(family, url, { weight: 400 })
.catch(() => console.log(`${family} did not load`)) // only the web rejects
sample.style({ fontFamily: family }) // the fallback font until the face lands
}
UIScreen(
sample,
UIButton(UIText("Use Lobster").style({ color: "#FFFFFF" }))
.style({ bgColor: "#007AFF", p: 12, borderRadius: 8 })
.onClick(() => useFont("Lobster", "https://example.com/fonts/Lobster-Regular.ttf")),
).style({ bgColor: "#FFFFFF", p: 16, gap: 12 }).open()- Give it a
.ttf/.otfURL; a project file goes throughfont("./…")instead. - The promise settles once the load is over. On iOS, Android, desktop and in headless tests it always resolves — a face that failed to load leaves the fallback; in a browser (the web, the web player) it rejects then.
- Await it before drawing text into a
Canvas: canvas text is baked once (canvas.md).
Pitfalls
// ✗ a type scale named `font` turns the macro off in the whole file
const font = { h2: { fontSize: 22, fontWeight: 700 } }
// ✓ any other name
const typeScale = { h2: { fontSize: 22, fontWeight: 700 } }// ✗ holding the first screen until a font has loaded — not needed
registerFont("Inter", "https://example.com/Inter.ttf").then(() => UIScreen(UIText("Home")).open())
// ✓ font() where the font is used; the screen opens at once
theme({ fontFamily: font("inter") })
UIScreen(UIText("Home")).open()See also
- Text & content elements —
UIText, thefontSize/fontWeight/fontStylestyles - Theme —
theme({ fontFamily }), scopes withel.theme() - Canvas — text drawn into a texture