Typography
Build a readable hierarchy while preserving spelling, attribution and the full text of a language entry.
Choose a role before a size
A type role expresses the job of a text block. A large heading introduces a page; a body paragraph carries its meaning; a label identifies a control. HTML heading levels describe the document structure independently of visual size.
| Role | Starting specification | Use and limits |
|---|---|---|
| Display | --text-5xl or --text-6xl; 1.15–1.25 leading | Short editorial introductions. Avoid dense reference pages and long unbroken text. |
| Page heading | --text-4xl; 700 weight; 1.25 leading | One h1 describing the current task. Allow wrapping at compact widths. |
| Section heading | --text-2xl; 600 weight; 1.3 leading | Use h2 for the next structural level, regardless of the selected size. |
| Body | --text-base; 400 weight; 1.5–1.75 leading | Definitions, instructions and source text. Limit long passages to roughly 65–72 characters per line. |
| Control label | --text-sm or --text-base; 500–600 weight | Persistent labels. Keep the visible label in the accessible name. |
| Supporting text | --text-sm; 400 weight; 1.5 leading | Metadata and help. Do not make essential instructions tiny. |
Scale and font stacks
The table below comes from the shared stylesheet. Sizes use rem so browser font preferences can resize them. The stylesheet requests Inter, Playfair Display and a system monospace stack; declaring a family does not download a font. Load licensed font files in the consuming app or accept the fallback. Verify actual glyph coverage rather than treating a named font as multilingual support.
| Token | Default value |
|---|---|
| --text-2xl | 1.5rem |
| --text-3xl | 1.875rem |
| --text-4xl | 2.25rem |
| --text-5xl | 3rem |
| --text-6xl | 3.75rem |
| --text-base | 1rem |
| --text-lg | 1.125rem |
| --text-sm | 0.875rem |
| --text-xl | 1.25rem |
| --text-xs | 0.75rem |
Reference roles and product recipes
The Material reference groups type into display, headline, title, body and label families, each with three sizes. Baseline and emphasized counterparts form two sets of fifteen. MobTranslate has a shared size scale and explicit page/control recipes; it does not export those thirty Material composite tokens. Choose only the roles the task needs, then document the full recipe rather than equating a size token with a complete type style.
Distinguish a short screen introduction from a secondary panel title, a long definition from a compact control label. Keep one structural heading hierarchy even when a section borrows a display treatment. A visually small heading remains a heading; a visually large number is not automatically one.
| Recipe field | Decision | Handoff check |
|---|---|---|
| Family and fallback | Readable body face; deliberate expressive display face | Actual licensed files, coverage and fallback order |
| Size and leading | Size role plus unitless line height | User font preferences, accents and wrapped controls |
| Weight and emphasis | Base/selected treatment with explicit semantics | Available weights, state information and reflow |
| Tracking and numeric features | Spacing unit and supported figure style | Actual font features and changing values |
| Font axes | Only those present in the delivered font | Binary ranges, defaults, overrides and fallback |
Baseline, emphasized and editorial treatments
Use a baseline recipe for ordinary reading and an emphasized counterpart for a deliberate moment of hierarchy: the current destination, an unread item or a consequential action. Keep the same role, compare line breaks and widths, and retain a textual/semantic state indicator. Making every label heavier removes the distinction. Our native controls retain their current styles; this guidance does not silently replace their tokens.
Separate editorial moments from functional labels. A collection introduction can celebrate content; a user-owned title can allow chosen presentation; a distinctive product feature can have an authored visual signature. Do not infer an emotional state or cultural identity from typed text and automatically choose a treatment. Keep source spelling and attribution readable.
Specify repeatable editorial recipes with the exact family, size, leading, tracking, weight and supported axes. Use one coherent treatment within a region, preserve the surrounding baseline system and test compact layouts before adopting a wider face. Motion remains optional; axis changes must not delay an action or obscure essential information.
Variable fonts and axis ownership
A variable file contains its own axes, ranges, defaults and sometimes hidden flags. A CSS property can be accepted even if the current font cannot use it. The local specimen below includes an unchanged Roboto Flex binary, its license, a frozen source URL and SHA-256. Build verification reads its actual fvar table and checks every displayed range; ordinary product stacks remain unchanged.
The inspection panel uses low-level font-variation-settings to expose this file precisely. These values override corresponding high-level settings; the ordinary weight selector is disabled while inspecting the variable file. In product CSS prefer high-level font-weight, font-stretch and font-style where they express the desired contract, and use custom axes only when needed. Axis tags are case-sensitive and ranges belong to this font.
The file marks several engineering axes hidden. They appear only under Advanced font inspection axes, for this specialized inspection task. Do not copy them wholesale into ordinary end-user preferences. Keep defaults for axes you have not deliberately designed and verified.
| Coordinate | Purpose | Verification |
|---|---|---|
| wght | Stroke weight | Compare actual small text and wrapping at extremes |
| GRAD | Fine optical weight adjustment | Compare strokes and measured advances; do not treat it as a color or contrast fix |
| wdth | Character width | Measure line length and reflow before using in compact controls |
| opsz | Size-specific drawing | Choose optical coordinates deliberately; it does not set CSS font-size |
| slnt | Slant angle in this file | Compare letterforms, readability and fallback |
Font delivery and fallback protection
Load only permitted files with recorded provenance, license and deployment paths. A stack declaration alone does not install Inter, Playfair Display or a multilingual collection. Font fallback can happen within a word as well as for a whole paragraph, changing metrics and line breaks. Confirm the actual required glyphs and shaping with source-attested permitted text and qualified readers.
Keep readable text during loading and failure. The specimen loads its local face when selected, exposes success/failure, retains user text and axes across family changes, and provides retry plus a return to fallback. Its ready state confirms a loaded face, not that every possible character comes from that face. It makes no global language-coverage promise.
This demonstration transfers the complete 1.8MB-class font only when selected. For a deployed product, assess licensed formats/subsets, cache behavior and required scripts; do not strip attested characters to reduce size. Compare fallback and final content while loading slowly or offline. A successful HTTP response alone does not prove text shaping or visual quality.
Language height, line boxes and baselines
The reference introduces four script-height categories: small, medium, large and extra large, with corresponding line-height and component adaptation. This web library does not implement automatic category detection or those native height tokens. Do not replace that missing capability with a hand-maintained language-name map. Validate the permitted scripts, delivered font metrics and actual reader preferences, then configure the relevant scope through a reviewed typography recipe.
A tall script, stacked mark or alternate fallback can need more vertical space at the same nominal size. Increase line height and allow controls/panels to grow; do not merely add blank space outside a clipped fixed-height label. The lab permits generous leading without changing spelling or assigning a language. For mixed-script content, inspect the combined line, rather than measuring one convenient Latin sample.
CSS lays out line boxes using font metrics and leading. DOM element height includes wrapping and padding; it is not a direct measurement of the ink or the baseline. Use baseline alignment when relating adjacent text, and specify padding/line-height for web containment. Translate design-tool baseline measurements into rendered CSS and verify the result instead of treating every box edge as an identical glyph boundary.
.reading-scope { line-height: 1.75; }
.reading-control {
min-block-size: 44px;
block-size: auto;
padding-block: .75rem;
white-space: normal;
}
.label-with-icon { display: inline-flex; align-items: baseline; gap: .5em; }Units, tracking, optical size and numbers
rem follows the root font size; em letter spacing follows the element font size. For a 24px element and 16px root, a desired 0.3px tracking distance is 0.0125em or 0.01875rem. Those values happen to agree only when the relevant font sizes agree. Android sp and web rem are platform conventions, not an unconditional device conversion: user preferences and the root size affect the rendered result.
Use unitless line-height for inheritable text recipes. Consider both line length and paragraph spacing, not just a single multiplier. The existing rem scale remains authoritative; adding a new product recipe does not require replacing all sizes with a mathematically generated scale.
font-optical-sizing: auto lets a supporting face choose optical treatment from text size. Manual opsz coordinates are independent; our inspector sets none and records the explicit axis. In ordinary CSS, avoid accidentally inheriting a large manual optical setting into a small label.
Tabular figures can stabilize changing clocks or aligned numeric columns when the chosen face supports them. They do not make all characters monospace, localize a number or guarantee equal widths in a fallback face. Use locale-aware formatting separately and compare real measurements. The specimen includes an explicit feature toggle and full current CSS.
Typography handoff and acceptance
- Record role and semantic element separately, plus family/files/license, base and emphasized recipes, leading/tracking units, numeric features and any axes. Include fallback behavior and the content/script scopes actually reviewed.
- Test short/long content, newlines, combining marks from permitted sources, user spacing and 200% text enlargement. Verify controls wrap and grow; preserve the full definition and citation.
- Compare exact loaded and failed/held font states without losing input or focus. Inspect the actual DOM text, requested/computed CSS and resulting line breaks, not just a font-family string.
- Check meaningful icon alignment, labels and target sizes separately from glyph size. For text contrast and underlined links, use the paired Colors/Accessibility guidance; variable axes do not certify contrast.
- Review emphasized/editorial moments in both themes and reduced motion. Match the treatment to the task through authored choices and user review, rather than a semantic keyword classifier.
Live type specimen
Change size, leading, tracking, numeric features and family. The optional local variable specimen loads a real licensed face and derives all axis controls from its binary metadata. Paste your own text to inspect wrapping; presentation is not language evidence.
The ordinary stacks are requests; the browser may use fallback. The local variable specimen has its own licensed font file and does not change product fonts. This is your input, not language evidence or script identification.
Current CSS and font provenance
font-family: var(--font-sans);
font-size: var(--text-2xl);
font-weight: 600;
line-height: 1.5;
letter-spacing: 0em;
font-variant-numeric: normal;
font-optical-sizing: auto;
font-variation-settings: normal;Manual opsz uses this font’s coordinates, independently of CSS text size. Changing size does not change that axis automatically.
Roboto Flex, unchanged binary, 1787292 bytes. License · Frozen upstream file. SHA-256: 9b523f7d82593df0107173849ebb8c817471a1df4b4fb2c3cbf40cfd810c8281.
Compiled typography inspector
'use client';
import { useEffect, useState } from 'react';
import catalogue from '../content/tokens.json';
import font from '../public/fonts/roboto-flex.json';
import './typography-explorer.css';
const sizes = catalogue.tokens.filter(token => token.name.startsWith('--text-')).sort((a, b) => parseFloat(a.value) - parseFloat(b.value));
const defaults = Object.fromEntries(font.axes.map(axis => [axis.tag, axis.default]));
export function TypographyExplorer() {
const [text, setText] = useState('Language first. Always.');
const [size, setSize] = useState('--text-2xl');
const [family, setFamily] = useState('--font-sans');
const [weight, setWeight] = useState('600');
const [leading, setLeading] = useState('1.5');
const [tracking, setTracking] = useState('0');
const [tabular, setTabular] = useState(false);
const [axes, setAxes] = useState(defaults);
const [loaded, setLoaded] = useState<'idle' | 'loading' | 'ready' | 'failed'>('idle');
const [attempt, setAttempt] = useState(0);
const variable = family === 'local-variable';
useEffect(() => {
if (!variable) return;
let current = true;
Promise.resolve().then(() => document.fonts?.load(`400 24px "${font.family}"`) ?? []).then(faces => {
if (current) setLoaded(faces.length > 0 && faces.every(face => face.status === 'loaded') ? 'ready' : 'failed');
}, () => { if (current) setLoaded('failed'); });
return () => { current = false; };
}, [variable, attempt]);
const activeFamily = variable ? loaded === 'ready' ? `"${font.family}", system-ui, sans-serif` : 'var(--font-sans)' : `var(${family})`;
const manualOptical = variable && loaded === 'ready';
const variation = font.axes.map(axis => `"${axis.tag}" ${axes[axis.tag]}`).join(', ');
const axisControls = (hidden: boolean) => font.axes.filter(axis => axis.hidden === hidden).map(axis => <label key={axis.tag}>
{axis.name} ({axis.tag}) · {axes[axis.tag]} <span className="type-axis-range">({axis.min} to {axis.max})</span>
<input data-type-axis={axis.tag} type="range" min={axis.min} max={axis.max} step="1" value={axes[axis.tag]} disabled={loaded !== 'ready'} onChange={event => setAxes(previous => ({...previous, [axis.tag]: Number(event.target.value)}))} />
</label>);
return <div className="type-lab" data-type-lab>
<label>Preview text<textarea data-type-text className="mt-input" rows={3} value={text} onChange={event => setText(event.target.value)} /></label>
<div className="grid gap-4 sm:grid-cols-2">
<label>Type size<select data-type-size className="mt-input" value={size} onChange={event => setSize(event.target.value)}>{sizes.map(token => <option key={token.name} value={token.name}>{token.name} · {token.value}</option>)}</select></label>
<label>Font family<select data-type-family className="mt-input" value={family} onChange={event => {setFamily(event.target.value);setLoaded(event.target.value === 'local-variable' ? 'loading' : 'idle');}}>{catalogue.tokens.filter(token => token.name.startsWith('--font-')).map(token => <option key={token.name} value={token.name}>{token.name}</option>)}<option value="local-variable">Local Roboto Flex specimen</option></select></label>
<label>Font weight<select data-type-weight className="mt-input" disabled={variable} value={weight} onChange={event => setWeight(event.target.value)}>{[400,500,600,700].map(value => <option key={value}>{value}</option>)}</select>{variable && <span>Use the wght axis below; it overrides this ordinary weight setting.</span>}</label>
<label>Line height<select data-type-leading className="mt-input" value={leading} onChange={event => setLeading(event.target.value)}>{[1.25,1.5,1.75,2,2.5,3].map(value => <option key={value}>{value}</option>)}</select></label>
<label>Letter spacing<select data-type-tracking className="mt-input" value={tracking} onChange={event => setTracking(event.target.value)}>{[0,0.02,0.05,0.12].map(value => <option key={value} value={value}>{value}em</option>)}</select></label>
<label className="type-checkbox"><input data-type-tabular type="checkbox" checked={tabular} onChange={event => setTabular(event.target.checked)} />Tabular numbers, when supported by the face</label>
</div>
{variable && <div>
<p role="status" data-type-font-status>{loaded === 'ready' ? 'Local variable font loaded. Axis controls are available.' : loaded === 'failed' ? 'Local font could not load. Text uses fallback; axes are unavailable. Choose another family or retry.' : 'Loading the local variable font. Text remains available in fallback.'}</p>
<fieldset><legend>Variable-font axes</legend><div className="type-axis-grid">{axisControls(false)}</div></fieldset>
<details data-type-advanced><summary>Advanced font inspection axes</summary><p>These axes carry the font’s hidden flag. They are exposed here for specialized font inspection, rather than as ordinary product preferences.</p><div className="type-axis-grid">{axisControls(true)}</div></details>
<button data-type-axis-reset type="button" onClick={() => setAxes({...defaults})}>Reset all axes</button>
<button data-type-font-retry type="button" aria-disabled={loaded === 'loading'} onClick={() => {if (loaded === 'loading') return;setLoaded('loading');setAttempt(previous => previous + 1);}}>Retry local font</button><button data-type-font-fallback type="button" onClick={() => {setFamily('--font-sans');setLoaded('idle');}}>Return to fallback family</button>
</div>}
<div data-typography-preview style={{fontSize:`var(${size})`,fontFamily:activeFamily,fontWeight:Number(weight),lineHeight:leading,letterSpacing:`${tracking}em`,fontVariantNumeric:tabular ? 'tabular-nums' : 'normal',fontOpticalSizing:manualOptical ? 'none' : 'auto',fontVariationSettings:variable && loaded === 'ready' ? variation : 'normal'}}>{text || 'Enter text to preview it.'}</div>
<p>The ordinary stacks are requests; the browser may use fallback. The local variable specimen has its own licensed font file and does not change product fonts. This is your input, not language evidence or script identification.</p>
<details data-type-css><summary>Current CSS and font provenance</summary><pre><code>{`font-family: ${activeFamily};\nfont-size: var(${size});\nfont-weight: ${weight};\nline-height: ${leading};\nletter-spacing: ${tracking}em;\nfont-variant-numeric: ${tabular ? 'tabular-nums' : 'normal'};\nfont-optical-sizing: ${manualOptical ? 'none' : 'auto'};\nfont-variation-settings: ${variable && loaded === 'ready' ? variation : 'normal'};`}</code></pre><p>Manual opsz uses this font’s coordinates, independently of CSS text size. Changing size does not change that axis automatically.</p><p>Roboto Flex, unchanged binary, {font.font.bytes} bytes. <a href={font.license.source}>License</a> · <a href={font.font.source}>Frozen upstream file</a>. SHA-256: <code>{font.font.sha256}</code>.</p></details>
</div>;
}
Inspector stylesheet
@font-face {
font-family: 'Styleguide Roboto Flex';
src: url('/fonts/roboto-flex.ttf') format('truetype');
font-weight: 100 1000;
font-stretch: 25% 151%;
font-style: normal;
font-display: swap;
}
.type-lab { display: grid; gap: 1rem; min-inline-size: 0; }
.type-lab label { display: block; min-inline-size: 0; overflow-wrap: anywhere; }
.type-lab .mt-input { display: block; inline-size: 100%; min-inline-size: 0; margin-block-start: .5rem; }
.type-lab textarea.mt-input { height: auto; min-block-size: calc(4.5em + 1.5rem + 4px); padding-block: .75rem; line-height: 1.5; resize: vertical; }
.type-lab fieldset { min-inline-size: 0; padding: 0; }
.type-axis-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1rem; }
.type-axis-grid input { display: block; inline-size: 100%; min-block-size: 44px; }
.type-lab .type-checkbox { display: flex; gap: .75rem; align-items: center; min-block-size: 44px; }
.type-lab .type-checkbox input { flex: 0 0 auto; }
.type-lab button { min-block-size: 44px; padding: .5rem .75rem; border: 1px solid var(--color-border); border-radius: var(--radius-md); margin: .5rem; }
.type-lab [data-typography-preview] { padding: 12px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow-wrap: anywhere; white-space: pre-wrap; }
.type-lab p, .type-lab summary, .type-lab code { overflow-wrap: anywhere; }
.type-lab pre { white-space: pre-wrap; max-inline-size: 100%; }
.type-lab a { text-decoration: underline; }
@media (forced-colors: active) { .type-lab [data-typography-preview], .type-lab button { border-color: CanvasText; } }
Frozen font manifest
{
"family": "Styleguide Roboto Flex",
"upstreamCommit": "e9678a5385322c63730122ba464765baad83cfd8",
"license": {
"file": "OFL-Roboto-Flex.txt",
"source": "https://raw.githubusercontent.com/google/fonts/e9678a5385322c63730122ba464765baad83cfd8/ofl/robotoflex/OFL.txt",
"bytes": 4488,
"sha256": "9cbaed04b20c853f99840efe5dc96956f6f6120ed83a0ade35f9281a2b63e5d0"
},
"font": {
"file": "roboto-flex.ttf",
"source": "https://raw.githubusercontent.com/google/fonts/e9678a5385322c63730122ba464765baad83cfd8/ofl/robotoflex/RobotoFlex%5BGRAD%2CXOPQ%2CXTRA%2CYOPQ%2CYTAS%2CYTDE%2CYTFI%2CYTLC%2CYTUC%2Copsz%2Cslnt%2Cwdth%2Cwght%5D.ttf",
"bytes": 1787292,
"sha256": "9b523f7d82593df0107173849ebb8c817471a1df4b4fb2c3cbf40cfd810c8281"
},
"axes": [
{
"tag": "opsz",
"min": 8.0,
"default": 14.0,
"max": 144.0,
"hidden": false,
"nameID": 256,
"name": "Optical Size"
},
{
"tag": "wght",
"min": 100.0,
"default": 400.0,
"max": 1000.0,
"hidden": false,
"nameID": 257,
"name": "Weight"
},
{
"tag": "GRAD",
"min": -200.0,
"default": 0.0,
"max": 150.0,
"hidden": false,
"nameID": 258,
"name": "Grade"
},
{
"tag": "wdth",
"min": 25.0,
"default": 100.0,
"max": 151.0,
"hidden": false,
"nameID": 259,
"name": "Width"
},
{
"tag": "slnt",
"min": -10.0,
"default": 0.0,
"max": 0.0,
"hidden": false,
"nameID": 260,
"name": "Slant"
},
{
"tag": "XOPQ",
"min": 27.0,
"default": 96.0,
"max": 175.0,
"hidden": true,
"nameID": 261,
"name": "Parametric Thick Stroke"
},
{
"tag": "YOPQ",
"min": 25.0,
"default": 79.0,
"max": 135.0,
"hidden": true,
"nameID": 262,
"name": "Parametric Thin Stroke"
},
{
"tag": "XTRA",
"min": 323.0,
"default": 468.0,
"max": 603.0,
"hidden": true,
"nameID": 263,
"name": "Parametric Counter Width"
},
{
"tag": "YTUC",
"min": 528.0,
"default": 712.0,
"max": 760.0,
"hidden": true,
"nameID": 264,
"name": "Parametric Uppercase Height"
},
{
"tag": "YTLC",
"min": 416.0,
"default": 514.0,
"max": 570.0,
"hidden": true,
"nameID": 265,
"name": "Parametric Lowercase Height"
},
{
"tag": "YTAS",
"min": 649.0,
"default": 750.0,
"max": 854.0,
"hidden": true,
"nameID": 266,
"name": "Parametric Ascender Height"
},
{
"tag": "YTDE",
"min": -305.0,
"default": -203.0,
"max": -98.0,
"hidden": true,
"nameID": 267,
"name": "Parametric Descender Depth"
},
{
"tag": "YTFI",
"min": 560.0,
"default": 738.0,
"max": 788.0,
"hidden": true,
"nameID": 268,
"name": "Parametric Figure Height"
}
],
"licenseName": "SIL Open Font License 1.1"
}
Language text and emphasis
- Preserve source spelling, diacritics, punctuation and whitespace where they carry meaning. Do not apply uppercase, title case or smart punctuation to dictionary text.
- Use semantic strong and em only when emphasis is intended. Weight alone must not encode review status.
- Keep phonetic transcription, source quotation and editorial explanation distinguishable with labels. Do not infer a language or pronunciation from typography.
- Use monospace for identifiers and code, not as a universal substitute for language fonts. Font fallback can change metrics, so avoid fixed-height text boxes.
Implementation
Keep selectable real text in the DOM. Do not rasterize headings or use images of text. Load only the weights used by the product and verify that fallback content remains readable while fonts load.
.entry-text {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: 1.75;
max-inline-size: 65ch;
overflow-wrap: anywhere;
}
.entry-heading { font-size: var(--text-2xl); line-height: 1.3; }Failure cases
- Avoid fixed block heights that clip accents or translated labels.
- Avoid truncating the only visible definition or source citation. Provide a deliberate expansion control for summaries.
- Avoid synthetic bold or decorative display faces for long reference passages.
- Do not shrink text to make a layout fit. Reflow the layout first.
Verification
- At 200% text enlargement and 400% browser zoom, inspect wrapping, control names, citations and focus visibility.
- Apply user text spacing: 1.5 line height, 2em paragraph spacing, 0.12em letter spacing and 0.16em word spacing. Content and controls must remain usable.
- Disable the preferred font and compare the fallback. Check the actual attested scripts used by the product with qualified readers.
- Inspect heading structure and accessible control names with a screen reader; the visual specimen cannot prove these.