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

TypeScript
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 it

Use 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.

TypeScript
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.
  • font is a macro only where it is the global: a file that declares or imports its own font (const font = { h2: … }, say) is skipped — every font("…") 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.

TypeScript
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.
  • italic needs 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 of rubik and for 400 / 700 of nunito montserrat ibm-plex-sans pt-sans jost exo-2 playfair-display lora pt-serif cormorant literata jetbrains-mono ibm-plex-mono; the other families have none (a compile error says so).
Note

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.

TypeScript
// 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 / .otf only: 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, like asset(); 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.

TypeScript
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:

TypeScript
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.json and FONT-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.

TypeScript
registerFont(fontFamily: string, url: string, options?: { weight?: number, style?: "normal" | "italic" }): Promise<void>
TypeScript
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 / .otf URL; a project file goes through font("./…") 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

TypeScript
// ✗ 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 } }
TypeScript
// ✗ 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, the fontSize / fontWeight / fontStyle styles
  • Theme — theme({ fontFamily }), scopes with el.theme()
  • Canvas — text drawn into a texture