PandaText / Guides / How to measure text the same way on every platform

How to measure text the same way on every platform

Web editors often measure text with Canvas measureText() and draw it on the page, or measure on one platform and draw on another. We measured the same strings both ways in Chrome 156, Safari 27.0.1 and Firefox 159 on one Mac. In the same browser, Canvas and the page disagreed by up to 50 px on one short line.

These are the three mismatches we found. In each picture, the green bar is the width of the text on the page. The red bar and the red line are the width Canvas reported: where a caret or a line break computed from Canvas would land.

1. Emoji: Canvas adds 4 to 5 px per emoji in Chrome and Firefox

Ten emoji after the word “Packing:” at 16 px were 227.2 px wide on the page in Chrome and Firefox. Chrome's Canvas reported 267.2 px, 4 px more per emoji, and Firefox's Canvas reported 277.2 px, 5 px more. Safari's Canvas and page agreed at 277.2 px, because Safari draws these emoji wider on the page too.

Ten emoji measured on the page and with Canvas. Chrome: 227.2 px on the page, 267.2 px in Canvas. Safari: 277.2 px in both. Firefox: 227.2 px on the page, 277.2 px in Canvas.
Green: width on the page. Red: width from Canvas measureText(). Chrome is 40 px off, Firefox 50 px; Safari matches.

A text box that wraps or places the caret with these numbers ends a line too early, or puts the caret 40 to 50 px past the last emoji. All three browsers use the same font here, Apple Color Emoji.

2. system-ui: Firefox's Canvas measures it 20 px wider

A sentence in 16 px system-ui, which is San Francisco on a Mac, was 249.6 px on the page in Firefox and 269.6 px in its Canvas. In Chrome and Safari, Canvas and the page agreed at 247.4 px. Firefox's page width also differed from the other two browsers, by 2.2 px.

A sentence in system-ui measured on the page and with Canvas. Chrome and Safari match at 247.4 px. Firefox: 249.6 px on the page, 269.6 px in Canvas.
system-ui at 16 px. Firefox's Canvas reports 20 px more than its page; Chrome and Safari match.

A Mozilla bug report describes the same kind of gap for OffscreenCanvas, where Firefox skips the automatic optical sizing that the page applies to the system font. This test used a regular canvas and saw it there too. App interfaces often use the system font, so this is where an editor is likely to meet it.

3. OffscreenCanvas ignores the page language

Two Chinese characters and a Korean word at 32 px, on a page marked as Korean, were 146.8 px wide in all three browsers. OffscreenCanvas, which is often used to measure text in a worker, reported 155.9 to 157.3 px. It has no page around it, so it does not know the text is Korean. Measured on their own, the Korean word matched, and the two Chinese characters made the whole gap: 55.4 px on the page against 64.0 to 65.3 px in OffscreenCanvas.

Chinese and Korean text in a Korean page, measured with OffscreenCanvas. The page width is 146.8 px in every browser; OffscreenCanvas reports 156.5 px in Chrome, 155.9 px in Safari and 157.3 px in Firefox.
OffscreenCanvas is 9 to 10.5 px off in every browser, because it does not know the page language.

Chinese, Japanese and Korean share many characters, and the language decides which font draws them. Code that measures in a worker has to carry the language with it, or measure with a font file that does not depend on it.

Native platforms have the same trap

Measuring on iOS and drawing with Android's text view uses two shapers, with different fonts, fallback fonts and line heights, so the drawn width is not the number you stored (see iOS and Android). Windows has it inside one API: DirectWrite's natural mode measures independently of the display, while its GDI modes round to it and change with DPI.

A wrong width also moves the caret. A tap becomes a string offset by walking the rectangles the painter used; if they came from another engine, the caret lands beside the wrong character. Mixed-direction lines make that worse.

Measure and draw with one engine

Measurement is the whole layout, not one number: the width, the line breaks and the caret rectangles, from the same engine that draws the text. Cache it with the text, the styles, the font files and the width as the key. A font that loads late is the classic miss: you measured with a fallback, painted with the real font, and the line grew.

On an edit, drop the cached lines from the edit down.

PandaText's measurement

PandaText does this measurement once for iOS, macOS, Android and the web. The web build runs as WebAssembly, so the browser does not substitute its own wrap or its own Canvas width. Platform input still produces the characters; the rectangles come from the shared layout.

Here is the packing list from the emoji test in PandaText, with the same emoji font on all six surfaces. The first line ends after the phone on each, and the second line is the same width.

The line “Packing list: 🧳🧴🪥…📚 done.” in PandaText on six surfaces. Each wraps after the phone emoji into two lines of the same width.
The packing list in PandaText, 17 pt in a box 300 wide, with Noto Color Emoji: two lines on the iPhone, Android, macOS, Chrome, Safari and Firefox.

How we tested

A test page draws each string in a span and measures it with Canvas measureText() in the same font, or with OffscreenCanvas for the Korean case, which we also split into its Chinese and Korean parts. Chrome 156, Safari 27.0.1 and Firefox 159 ran in normal windows on a Retina Mac with macOS 27.0.1, in October 2026.

Two more cases matched in all three browsers: kerning around spaces in Noto Sans, and letter-spacing. Earlier reports found a kerning gap in Chrome's Canvas with Arial-like fonts; we did not see it with Noto Sans.

Check that a measurement will draw the same

  1. Measure the risky strings. Compare Canvas measureText() with the page for emoji, the system font and text in other languages, in every browser you support.
  2. Query the full layout. Record the width, the line breaks and the caret rectangles from one engine, for one font file and one column width.
  3. Draw that result. Paint those lines on each platform. Do not pass the string back through the platform's own wrapping.
  4. Reload the font. Repeat after the real font file has loaded. A match against the fallback font will drift once the font arrives.

Questions

Is a string-width function enough?

No. The width of a string does not tell you where the line broke, and it does not give you the caret rectangle for a tap inside the line.

Can I measure with Canvas and draw with the DOM?

Not reliably. In this test, Canvas was 40 to 50 px off for ten emoji in Chrome and Firefox, 20 px off for system-ui in Firefox, and OffscreenCanvas was 9 to 10.5 px off for Chinese characters on a Korean page in all three browsers.

Sources

Related guides