@canonical/styles-typography
v0.20.0
Published
Typography baseline alignment engines and scale tokens for the Canonical Design System.
Downloads
226
Maintainers
Keywords
Readme
@canonical/styles-typography
Baseline grid alignment for the Canonical Design System. This package provides three interchangeable CSS engines that snap text baselines to a configurable grid, plus a semantic token mapper that bridges @canonical/design-tokens typography tokens to the engine's variable contract.
Quick Start
Import the default engine (cap-unit) and set your baseline height:
@import url("@canonical/styles-typography");
:root {
--baseline-height: 0.5rem;
}That's it. All h1–h6 and p elements will align to the baseline grid. The default engine uses the CSS cap unit and requires no JavaScript font extraction.
How It Works
The browser adds invisible half-leading above and below each line of text. The exact amount depends on the font's internal metrics, the computed font-size, and line-height. This makes vertical alignment between different text elements unpredictable.
The baseline engines solve this by computing where the first baseline falls within a line box, then applying a padding-top / margin-bottom pair that nudges the element so its baseline lands exactly on a grid line. The complementary margin-bottom ensures the element's total outer height remains a multiple of --baseline-height.
line-height (computed)
+----------------------------------------------+
| half-leading (top) |
| +----------------------------------------+ |
| | ascender area | |
| | - - - - cap line - - - - - - - - - - - | |
| | glyph body | |
| | - - - - baseline - - - - - - - - - - - | | ← snap this to the grid
| | descender area | |
| +----------------------------------------+ |
| half-leading (bottom) |
+----------------------------------------------+The mod() CSS function does the heavy lifting:
--top-nudge: calc(
var(--baseline-height) -
mod(var(--baseline-position), var(--baseline-height))
);Multi-line blocks stay on-grid because line-height is always set to a multiple of --baseline-height. The nudge only compensates for the first line's half-leading offset.
Engines
Three engines implement the same grid-snap pattern with different trade-offs. Import the one that fits your constraints directly, or use index.css which re-exports the default (cap-unit).
baseline-cap.css — Cap unit (default)
@import url("@canonical/styles-typography/src/baseline-cap.css");Uses the browser-native cap CSS unit to resolve font metrics at render time. No JavaScript extraction step, no per-font variables. Changing font-family on an element automatically updates the 1cap value the engine uses.
The baseline position formula is (line-height + 1cap) / 2 — the browser resolves 1cap from the font's OpenType tables natively.
| Browser | Minimum version | |---------|-----------------| | Chrome | 117+ | | Safari | 17.2+ | | Firefox | 97+ |
baseline-metrics.css — Extracted metrics
@import url("@canonical/styles-typography/src/baseline-metrics.css");The original engine with the widest browser support. Requires three CSS variables extracted from the font file using the extract-font-data CLI:
:root {
--ascender: 1068;
--descender: -292;
--units-per-em: 1000;
}The baseline position is computed from these metrics: ((line-height - line-height-scale) / 2) + ascender-scale. More verbose, but works everywhere mod() is supported.
baseline-trim.css — Text-box-trim hybrid
@import url("@canonical/styles-typography/src/baseline-trim.css");The most modern approach. Uses text-box: trim-both cap alphabetic to remove half-leading entirely, then compensates with mod()-based margin to restore grid alignment. Results in tighter content boxes (useful for buttons, cards, optical centering).
| Browser | Minimum version | Notes | |---------|-----------------|-------| | Chrome | 133+ | | | Safari | 18.2+ | | | Firefox | — | Not yet implemented |
Falls back gracefully: if text-box-trim is unsupported, the element keeps its default half-leading and the nudge still applies.
Consumer Contract
Every engine reads the same set of CSS custom properties per element:
| Variable | Scope | Description |
|----------|-------|-------------|
| --baseline-height | :root | Grid unit size (e.g. 0.5rem) |
| --font-size | element | Font size as a <length> |
| --line-height-multiplier | element | Line height in baseline-height units |
| --line-height | element | Optional override: explicit line height, bypasses the multiplier |
| --space-after | element | Optional: extra bottom margin in baseline-height units |
The metrics engine additionally requires on :root:
| Variable | Description |
|----------|-------------|
| --ascender | Font ascender value (unitless, from OpenType tables) |
| --descender | Font descender value (unitless, negative) |
| --units-per-em | Font units-per-em value |
Token Mapper
The mapper.css file bridges the semantic typography tokens from @canonical/design-tokens to the engine's variable contract. It is imported automatically by the default engine (baseline-cap.css).
The design tokens provide variables like:
--typography-heading-1-font-size
--typography-heading-1-line-height (unitless ratio)
--typography-heading-1-font-weight
--typography-heading-1-letter-spacing
--typography-heading-1-font-familyThe mapper converts these into the engine variables for each element (h1–h6, p), including computing --line-height-multiplier by snapping the typographic line-height to the nearest baseline-grid unit:
--line-height-multiplier: round(
up,
calc(font-size × line-height-ratio / baseline-height),
1
);Package Structure
src/
index.css ← re-exports baseline-cap.css (default)
baseline-cap.css ← cap-unit engine
baseline-metrics.css ← extracted-metrics engine
baseline-trim.css ← text-box-trim + cap hybrid
mapper.css ← semantic token → engine variable bridge
scripts/
extractFontData.ts ← CLI for extracting font metrics
example/
index.html ← interactive demo with engine switcher
serve.ts ← dev server with live-reload
scripts/ ← sidebar, font picker, content presets
fonts/ ← bundled .woff2 files for the demo
styles/ ← demo layout and debug stylesExtracting Font Metrics
The extract-font-data CLI reads OpenType metrics from a font file. This is only needed when using the baseline-metrics engine.
bun run extract-font-data ./path/to/font.ttfOutput:
Add the following variables to your CSS and follow the instructions in the README:
:root {
--ascender: 1068;
--descender: -292;
--units-per-em: 1000;
}Pass --all to see the full metrics table including computed nudge values for a given line-height:
bun run extract-font-data ./path/to/font.ttf 1.5 --allCaveats
The extractor uses opentype.js to parse font files. It works with most .ttf, .otf, and .woff files but may fail on fonts without TrueType or CFF outlines:
error: Font doesn't contain TrueType or CFF outlines.Development
Start the interactive example with live-reload:
bun run devThis serves the example at http://localhost:3333/example/. The sidebar lets you:
- Switch engines — toggle between cap-unit, metrics, and text-trim in real time
- Switch fonts — pick from bundled fonts with auto-applied metrics
- Tune per-tag — adjust font-size, line-height multiplier, and space-after for each heading level and paragraph
- Adjust baseline — change the baseline grid height and see the alignment update
The baseline grid is rendered as a red 1px line overlay so alignment errors are immediately visible.
Browser Support
All engines require mod() for the grid-snap calculation:
| Browser | mod() support |
|---------|-----------------|
| Chrome | 125+ |
| Safari | 17.4+ |
| Firefox | 128+ |
The cap-unit and text-trim engines additionally require the cap CSS unit (Chrome 117+, Safari 17.2+, Firefox 97+) and round() function respectively. See each engine section above for specific requirements.
