Typography tokens
Three system font stacks, a seven-step fluid scale whose slopes are calculated rather than guessed, four line heights sized for Cyrillic, and the small set of weight, tracking and numeral controls that everything else is built from.
Font stacks
Three tokens, and all three are system stacks. That is a decision, not an omission: zero network requests, zero flash of unstyled text, no font files to self-host, no licence to track, and nothing that can fail to load on a slow connection. Every face named in the stacks carries full Mongolian Cyrillic coverage, including ө and ү — the two characters that most Latin-first stacks fall back to a different face for, producing a visible mismatch mid-word.
A system stack renders differently on Windows, Android and iOS. The same heading is Segoe UI on a desktop, Roboto on a Pixel and San Francisco on an iPad, and their metrics do not match. For internal tooling and the kiosk app that is a fair price for the load behaviour. For anything print-adjacent — anything where a fixed line count or a fixed page break matters — check it on all three before signing off, because the wrap points will differ.
Sans
Everything by default: body copy, headings, controls, nav.
Create a new order
Шинэ захиалга үүсгэх
Payment details were updated successfully.
Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.
Mono
Code, token names, identifiers, and anything that has to align by column.
Create a new order
Шинэ захиалга үүсгэх
Payment details were updated successfully.
Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.
Display
Large headings. Currently an alias of the sans stack — a named seam, so a display face can be introduced later without touching call sites.
Create a new order
Шинэ захиалга үүсгэх
Payment details were updated successfully.
Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.
The fluid scale
Seven steps, --text-xs through --text-3xl, each a single
clamp(). The slopes are derived rather than eyeballed. For a 16px root,
1vi is V/1600 rem where V is the viewport inline size
in pixels. Anchoring the minimum at 20rem (320px) and the maximum at 96rem (1536px) means the
vi term has to cover the difference across
0.96 - 0.2 = 0.76:
slope = (max - min) / 0.76
intercept = min - 0.2 * slope
/* --text-lg: min 1.125rem, max 1.375rem */
slope = (1.375 - 1.125) / 0.76 = 0.3289
intercept = 1.125 - 0.2 * 0.3289 = 1.0592
--text-lg: clamp(1.125rem, 1.0592rem + 0.3289vi, 1.375rem);The unit is vi rather than vw, so the scale stays correct if a
consumer ever switches writing-mode.
An earlier set of slopes topped out around 1230px instead of 1536px. Nothing looked broken
— the type simply stopped growing two-thirds of the way to the
--size-container-2xl ceiling, and every wide layout read a step too small. The
two formulas are a pair: change either the minimum or the maximum and rerun both. Changing
one anchor and leaving the slope alone is exactly how that bug happened.
Specimen
Latin on the left, Mongolian Cyrillic on the right, in equal columns at the step's own size. The length difference is the point: the same message runs roughly 15–25% longer, and it is a layout constraint, not a translation problem.
| Token | Definition | Latin / Cyrillic |
|---|---|---|
--text-xs |
|
Updated 2 minutes ago 2 минутын өмнө шинэчлэгдсэн |
--text-sm |
|
User settings Хэрэглэгчийн тохиргоо |
--text-base |
|
Payment details were updated successfully. Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ. |
--text-lg |
|
Create a new order Шинэ захиалга үүсгэх |
--text-xl |
|
Account balance Дансны үлдэгдэл |
--text-2xl |
|
Transfer confirmed Гуйвуулга баталгаажлаа |
--text-3xl |
|
Welcome back Дахин тавтай морил |
Line height
Four steps. --leading-normal is deliberately more generous than a Latin-only
system would choose, because Cyrillic sets taller at the same font size: the x-height is
larger relative to the em, and descenders on
у, р and
ц reach further down. A leading that reads as comfortable in
English closes up in Mongolian.
--leading-tight is for display headings only. It is safe on two or three words
set large; it is not safe on running Cyrillic, where the descenders start touching the line
below.
Each block below is the same pair of paragraphs at a narrow measure, so both wrap to several lines and the difference is visible rather than theoretical.
Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.
Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.
Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.
Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.
Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.
Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.
Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.
Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.
| Token | Value | Use |
|---|---|---|
--leading-tight | | Display headings only. Do not put running Cyrillic on it. |
--leading-snug | | Sub-headings and short blocks that are read in one glance. |
--leading-normal | | Body default. Deliberately generous — see below. |
--leading-loose | | Long-form reading, and dense Cyrillic tables. |
Weight, tracking and numerals
Weight
Four steps, no more. The system stacks do not all ship every intermediate weight, and asking for one that does not exist gets you a synthesised face that looks wrong in Cyrillic before it looks wrong in Latin.
| Token | Value | Latin | Cyrillic |
|---|---|---|---|
--weight-regular | | Account balance | Дансны үлдэгдэл |
--weight-medium | | Account balance | Дансны үлдэгдэл |
--weight-semibold | | Account balance | Дансны үлдэгдэл |
--weight-bold | | Account balance | Дансны үлдэгдэл |
Tracking
| Token | Value | Latin | Cyrillic | Use |
|---|---|---|---|---|
--tracking-tight | | Create a new order | Шинэ захиалга үүсгэх | h1 and h2 only. Large type needs the correction; small type does not. |
--tracking-normal | | Create a new order | Шинэ захиалга үүсгэх | Everything else. An explicit token so a component can reset back to it. |
--tracking-wide | | Create a new order | Шинэ захиалга үүсгэх | Small uppercase-ish labels and eyebrow text. Use it on Latin; on Cyrillic it widens an already longer string. |
Tabular numerals
--font-numeric-tabular is a font-feature string, not a length:
. tnum makes every digit the
same advance width so columns line up; lnum forces lining figures so digits do
not sit at different heights beside each other. Currency columns need both.
| Without | With |
|---|---|
| 1,240,500 ₮ | 1,240,500 ₮ |
| 98,013 ₮ | 98,013 ₮ |
| 7,777,777 ₮ | 7,777,777 ₮ |
| 111,111 ₮ | 111,111 ₮ |
| 640,020 ₮ | 640,020 ₮ |
src/base.css wires this up for you. Any cell marked
data-numeric gets end alignment and both features, so a table author never
writes the token by hand:
td[data-numeric],
th[data-numeric] {
text-align: end;
font-variant-numeric: tabular-nums;
font-feature-settings: var(--font-numeric-tabular);
}<table>
<thead>
<tr><th scope="col">Account</th><th scope="col" data-numeric>Balance</th></tr>
</thead>
<tbody>
<tr><th scope="row">5401 2233</th><td data-numeric>1,240,500</td></tr>
</tbody>
</table>Underlines
Two tokens, applied by base.css to links.
| Token | Value | Why |
|---|---|---|
--text-underline-offset |
|
Em-relative, so it tracks the type scale instead of drifting at the large steps. A
pixel offset that looks right at --text-sm is a hairline at
--text-3xl. It is not zero because Cyrillic descenders reach further
down than Latin ones: at zero offset the rule cuts through
у and ц rather than passing
under them. |
--text-underline-thickness |
|
from-font defers to the face's own metric where one exists, which is
what the Cyrillic faces in the stack want. Where none exists the browser falls back to
its default, which is correct for that face. |
How headings map
src/base.css assigns one scale step per heading level. Nothing else in the
system sets a heading size, so this table is the whole mapping.
| Element | Size | Weight | Leading | Tracking |
|---|---|---|---|---|
h1 | --text-3xl | --weight-bold | --leading-tight | --tracking-tight |
h2 | --text-2xl | --weight-semibold | --leading-tight | --tracking-tight |
h3 | --text-xl | --weight-semibold | --leading-snug | --tracking-normal |
h4 | --text-lg | --weight-semibold | --leading-snug | --tracking-normal |
h5 | --text-base | --weight-medium | --leading-snug | --tracking-normal |
h6 | --text-sm | --weight-medium | --leading-snug | --tracking-normal |
h6 is deliberately not uppercased. The usual small-caps or
text-transform: uppercase treatment for the smallest heading costs nothing in
Latin, but Mongolian Cyrillic uppercase runs materially wider than lowercase — on an
element already sitting at --text-sm, that is exactly where a label starts
wrapping or clipping. If you want the eyebrow look, reach for
--tracking-wide instead and leave the case alone.