Engineering

Font loading and layout shift in editors

A web font that arrives late reflows the text under the caret. Choose font-display on purpose, wait for fonts before measuring, and store selections as document positions.

6 min read

An editor opens, the words appear in a fallback font, and a moment later the real font loads and every line reflows. The caret that was blinking after the first word jumps to a new position. A writer who began typing in that first half-second finds their text has landed in a different place than they meant. The cause is ordinary: web fonts load after the first paint, and the editor measured the text with the wrong metrics.

Layout depends on the font that was measured

The browser lays out text using the font it has at that moment. If the editor reads line breaks, widths or the position of the caret before the web font arrives, those values are stale as soon as the font swaps in. The same applies to any code that measures text to place a popup, a comment marker or a selection highlight. A value read once and cached will be wrong for the rest of the session.

Choose the font-display strategy on purpose

The font-display setting decides what happens while the font loads. swap shows the fallback immediately and replaces it later, which is fast but causes the reflow described above. optional uses the web font only if it arrives within a very short window, and otherwise keeps the fallback for the whole visit, which avoids the jump entirely at the cost of the typeface on a slow connection. For an editor, where the text position matters more than the exact typeface, optional is often the better choice. Pick it deliberately, and do not inherit a framework default without checking.

Code example
@font-face {
  font-family: "Editor Sans";
  src: url("/fonts/editor-sans.woff2") format("woff2");
  font-display: optional; /* keep the fallback if the font is not ready in time */
}

.editor-content {
  font-family: "Editor Sans", system-ui, sans-serif;
  /* Match the fallback's metrics so the swap changes less, when it does happen */
  font-size-adjust: 0.5;
}

The font-size-adjust line keeps the x-height of the fallback close to the web font, so the change in line height and width is smaller when the swap happens. Check the value against your own font, because the right number depends on the typeface.

Wait for the font before measuring

When the editor must measure text, wait for the fonts to be ready first. The document.fonts.ready promise resolves once the fonts the page has requested have loaded or failed. Run the first measurement after it resolves, and run it again if the fonts change later. Listen for the loadingdone event on document.fonts to catch a font that loads after the first check. Do not block the editor from appearing while waiting; show the content, and correct the positions when the measurement is ready.

Keep the caret stable through the swap

A caret that has been placed by character index keeps its place in the text, so a reflow moves it to the same character on the new line. A caret placed by pixel position does not, and it can land on a different word. Store the selection as document positions, not coordinates, and recompute the on-screen position after a reflow. The writer then sees the caret stay on the word they were typing.

A short test list


RichTextEditor is a perpetual-licence JavaScript editor — one purchase, self-hosted, no metered editor loads. Download the evaluation or see how it compares.