Typography
Typography
This site sets Latin text in Geist, monospace in Geist Mono, and Chinese in Noto Sans SC. All three are OFL-licensed and served from our own origin, with the Chinese face split into 97 unicode-range subsets that load on demand. Sizes follow the Tailwind CSS v4 text-* scale.
Type scale
Each row is rendered at its real size.
text-xs12pxtext-sm14pxtext-base16pxtext-lg18pxtext-xl20pxtext-2xl24pxtext-3xl30pxtext-4xl36pxtext-5xl48pxFont weights
font-normal400font-medium500font-semibold600font-bold700Font stack
Two runtime variables drive every font in the library; no component hard-codes a family. A project that never sets them renders exactly as it did before this token layer existed.
--hl-font-sans: Geist, ui-sans-serif, system-ui,
"PingFang SC", "Microsoft YaHei", sans-serif;
--hl-font-mono: "Geist Mono", ui-monospace, Menlo, monospace;Use your own fonts
Set the variables on :root and you are done. The 48 components that use font-mono follow along.
:root {
--hl-font-sans: "Your Sans", ui-sans-serif, system-ui, sans-serif;
--hl-font-mono: "Your Mono", ui-monospace, monospace;
}Change one region only
Set the same variable on a container. Body text and monospace are not symmetric here: monospace needs only the variable, while body text also needs a font-sans class on the container. Custom properties inherit, but a font-family that reads a variable is re-resolved only on elements that declare it, so ordinary text inherits the already-resolved family from the root and never looks at the variable again.
{/* Monospace: the variable alone is enough, since font-mono elements re-resolve it in their own scope */}
<div style={{ "--hl-font-mono": '"IBM Plex Mono", monospace' }}>
<Snippet>pnpm add @hulianui/ui</Snippet>
</div>
{/* Body text: the variable plus a font-sans class, or the text keeps the family already resolved at the root */}
<div className="font-sans" style={{ "--hl-font-sans": "Georgia, serif" }}>
This block is set in Georgia
</div>How the Chinese face is served
A full Noto Sans SC weighs megabytes and would stall first paint, so it is split into 97 unicode-range subsets and the browser fetches only the ones a page actually uses (15 on this page). Each subset is itself a variable font, so one set covers every weight. In the stack, the CJK family must come before ui-sans-serif and system-ui: those generic families resolve Han characters to the system font, and anything listed after them never gets a turn.
/* The stack this site actually ships: Geist for Latin, Noto Sans SC for Chinese, system fonts for the rest */
--hl-font-sans: Geist, "Noto Sans SC", ui-sans-serif, system-ui,
"PingFang SC", "Microsoft YaHei", sans-serif;