Skip to content

feat: resolve rem/em font units to px before canvas measurement#109

Open
okaris wants to merge 2 commits intochenglou:mainfrom
okaris:feat/resolve-relative-font-units
Open

feat: resolve rem/em font units to px before canvas measurement#109
okaris wants to merge 2 commits intochenglou:mainfrom
okaris:feat/resolve-relative-font-units

Conversation

@okaris
Copy link
Copy Markdown

@okaris okaris commented Apr 6, 2026

Problem

Canvas ctx.font silently ignores relative CSS units (rem, em). When callers pass CSS-derived font strings like "0.9rem Inter", canvas keeps its previous font and returns wrong widths. This is a spec limitation — canvas can exist without a document root, so relative units have no reference point (WHATWG HTML #1682).

In practice, anyone using Tailwind or a CSS framework with custom text-sm sizes (e.g. --text-sm: 0.9rem instead of the default 0.875rem) gets silently wrong measurements. The text measures at whatever font was previously set on the canvas context, not at the intended size.

Related: #107 (canvas doesn't account for certain CSS properties).

Solution

Add resolveFont() in measurement.ts that rewrites relative units to absolute px before the string reaches canvas:

"0.9rem Inter" → "14.4px Inter"  (given 16px root)
"bold 1.5em mono" → "bold 24px mono"
"14px Inter" → "14px Inter"  (no-op, no allocation)

Resolution uses getComputedStyle(document.documentElement).fontSize — the same level of DOM access that getEmojiCorrection() already does. Results are cached at module level so the DOM read happens once. Environments without a document (OffscreenCanvas, Node.js) fall back to 16px.

Changes

  • resolveFont(font) — exported, cached, handles rem/em → px
  • getFontMeasurementState() — resolves font before ctx.font = and cache keying
  • parseFontSize() — resolves before extracting the px value
  • clearMeasurementCaches() — clears the resolution cache + cached root font size
  • Tests for px passthrough, rem resolution, em resolution, and parseFontSize integration

Test plan

  • bun test src/layout.test.ts — 85 tests pass (5 new)
  • Existing tests unaffected (all use px fonts, passthrough is zero-cost)
  • No document in test env → falls back to 16px root, rem math still correct

okaris added 2 commits April 6, 2026 12:54
Canvas ctx.font silently ignores relative CSS units like rem and em
(spec limitation — WHATWG HTML #1682). Callers passing CSS-derived font
strings such as "0.9rem Inter" get no measurement at all; canvas keeps
the previous font and returns wrong widths.

This adds resolveFont() which rewrites relative units to absolute px
using getComputedStyle(document.documentElement).fontSize before the
string reaches canvas. Results are cached at module level so the DOM
read happens once. Environments without a document (OffscreenCanvas,
Node) fall back to 16px.

- getFontMeasurementState() now resolves the font before setting ctx.font
- parseFontSize() resolves before extracting the px value
- clearMeasurementCaches() clears the resolution cache
- resolveFont() is exported for direct use by consumers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant