To measure DOM text reliably, wait for the relevant stylesheet, put the text in its intended layout, then await document.fonts.ready before reading geometry. This guide shows the sequence in a Mac desktop browser and explains when to repeat it.

The task here is measuring a synthetic label or wrapped paragraph in a browser page—for example, deciding how much space a card needs. The browser should perform the text layout; your code should establish readiness and read the resulting boxes.

Which readiness signal should you wait for?

There are separate milestones for document construction, stylesheet loading and font layout. Choose the milestone that matches the work you control, then measure after the content and its styling are in place.

Signal What it establishes What still needs attention
DOMContentLoaded Parsing and deferred script execution have completed It does not directly wait for stylesheets
A stylesheet’s load event That stylesheet and its imported content have loaded and parsed Font loading and the target’s resulting layout
The window’s load event Initial page resources, including stylesheets, have loaded, except lazy resources Later application changes and font readiness
document.fonts.ready Loading and layout involving the document’s used fonts have completed Subsequent content, style or size changes

MDN documents the stylesheet-related nuance: DOMContentLoaded itself does not wait for stylesheets, although deferred scripts wait for them and can therefore delay the event indirectly. It is useful for finding elements, but it is an incomplete measurement contract. Document: DOMContentLoaded event.

For a stylesheet you insert yourself, attach its load and error handlers before inserting the link. MDN places the stylesheet’s load event after loading and parsing, immediately before the styles begin applying. WHATWG’s stylesheet processing also distinguishes successful loading from an error event. Stylesheet load events, HTML Standard: Stylesheet links.

For a page’s initial resource load, the window’s load event is a broader checkpoint. A script starting afterward must check document.readyState before subscribing, because an event that already fired will not run a newly attached handler. Application code that inserts a stylesheet later still needs to track that insertion explicitly. Window: load event, Checking document readiness.

What does font readiness actually guarantee?

The W3C CSS Font Loading specification explains that ready fulfills after the relevant layout operations complete and no further font loads are needed for that work. This includes situations where font fallback causes additional loading. The same promise fulfills only once, and later changes can cause further font loading. Read document.fonts.ready after the changes relevant to each measurement instead of keeping one startup promise forever. CSS Font Loading Module Level 3: The ready attribute.

Readiness concerns the fonts used by the document. It does not mean every declared font downloaded. MDN specifically notes that font-display: optional can make the used font set differ from the declared set. A completed measurement may therefore describe fallback text correctly. Document: fonts property.

Decide which outcome you need before writing the waiting code:

  • Measure the current rendered result: await font readiness and read the DOM geometry, accepting the browser’s fallback decisions.
  • Require a declared downloadable face: explicitly load the matching face, handle failure, then await document font readiness before measuring.
  • Measure changing content: repeat the sequence after each relevant update and discard results superseded by another update.

For the second outcome, document.fonts.load() accepts a CSS font specification and optional sample text. With a synthetic declared family called SampleFace, the specification could be 400 18px "SampleFace", and the text could be Panel Alpha. The returned promise rejects if a matched face fails to load. The text argument selects faces through their declared Unicode ranges; it does not verify individual glyph coverage. FontFaceSet: load() method.

Do not substitute document.fonts.check() for that requirement. MDN explains that it may return true even when the requested font is unavailable. Likewise, the W3C specifies that ready never rejects, so successful completion alone does not prove font-loading success. FontFaceSet: check() method, CSS Font Loading: The ready attribute.

Run a complete stylesheet-to-measurement example

On your Mac, open about:blank in a current desktop browser, open its developer tools and select the Console. Run the following complete JavaScript program once. It creates its own neutral sample text and stylesheet, so it needs no server, library or font file.

The example deliberately uses system-ui. It demonstrates the waiting sequence without depending on a downloadable asset. On a page using web fonts, keep the sequence and let that page’s actual font declarations supply the resources. The browser example has been inspected but was not executed for this manuscript.

(async () => {
  if (!document.fonts || !document.body) {
    throw new Error("A document body and CSS Font Loading support are required.");
  }
  if (document.getElementById("text-measure-demo")) {
    throw new Error("Reload the blank tab before running this example again.");
  }

  async function withDeadline(promise, label, milliseconds = 10000) {
    let timer;
    try {
      return await Promise.race([
        promise,
        new Promise((_, reject) => {
          timer = setTimeout(() => {
            reject(new Error(`${label} did not finish within the deadline.`));
          }, milliseconds);
        })
      ]);
    } finally {
      clearTimeout(timer);
    }
  }

  const paragraph = document.createElement("p");
  paragraph.id = "text-measure-demo";
  paragraph.textContent =
    "Panel Alpha contains a compact label. Panel Beta contains another label.";
  document.body.append(paragraph);

  const css = `
    #text-measure-demo {
      box-sizing: border-box;
      width: 280px;
      margin: 24px;
      padding: 12px;
      border: 1px solid #777;
      font: 400 18px/1.5 system-ui, sans-serif;
      letter-spacing: normal;
      white-space: normal;
      overflow-wrap: normal;
    }
  `;
  const stylesheetURL = URL.createObjectURL(
    new Blob([css], { type: "text/css" })
  );
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = stylesheetURL;

  const stylesheetLoaded = new Promise((resolve, reject) => {
    link.onload = resolve;
    link.onerror = () => reject(new Error("The sample stylesheet failed."));
  });

  try {
    document.head.append(link);
    await withDeadline(stylesheetLoaded, "Stylesheet loading");

    // Establish the target's styled layout before reading font readiness.
    paragraph.getBoundingClientRect();
    await withDeadline(document.fonts.ready, "Font readiness");

    const style = getComputedStyle(paragraph);
    const range = document.createRange();
    range.selectNodeContents(paragraph);
    const copyRect = rect => ({
      x: rect.x, y: rect.y, width: rect.width, height: rect.height
    });

    console.log({
      text: paragraph.textContent,
      fontFamily: style.fontFamily,
      fontSize: style.fontSize,
      lineHeight: style.lineHeight,
      elementBox: copyRect(paragraph.getBoundingClientRect()),
      textFragments: Array.from(range.getClientRects(), copyRect)
    });
  } catch (error) {
    link.remove();
    paragraph.remove();
    throw error;
  } finally {
    link.onload = null;
    link.onerror = null;
    URL.revokeObjectURL(stylesheetURL);
  }
})().catch(error => console.error(error));

The important order is stylesheet completion, established target layout, font readiness, then the final geometry reads. The first geometry read establishes the current layout; its value is intentionally discarded. The final reads describe the result after font readiness. The ten-second deadline is an illustrative operational limit: expiration reports an unfinished attempt and produces no measurement.

Inspect fontSize and lineHeight alongside the boxes when adapting the example. getComputedStyle() returns resolved property values after active stylesheets have been applied, making it useful for detecting a selector or cascade mismatch. Its fontFamily value describes the CSS family list; it should not be treated as proof of which face supplied every glyph. Window: getComputedStyle() method.

Which DOM measurement answers your question?

Use element.getBoundingClientRect() when you need the outer space occupied by a card or paragraph. Its rectangle includes padding and borders, and its position is relative to the viewport. In the sample, the declared border-box width is 280px; the text’s available content width is 254px, calculated as 280 − 24 − 2. That calculation describes the supplied CSS, not an observed browser result. Element: getBoundingClientRect() method.

Use a Range over the text when you need its layout fragments. Range.getClientRects() returns rectangles for the range rather than just the enclosing paragraph box. Keep the collection: flattening wrapped text into one width loses useful information about its occupied regions. Do not name the array length lineCount in a general implementation; complex inline content can contribute multiple rectangles. Range: getClientRects() method.

Keep the original text run intact while measuring. Adding a wrapper around every character can introduce formatting boundaries that affect shaping. W3C’s CSS Text draft specifies conditions under which inline boundaries preserve or break shaping. Measuring the existing DOM with a range avoids introducing those wrappers. CSS Text Module Level 3: Shaping Across Element Boundaries.

Canvas has a different role. measureText() returns canvas text metrics for a supplied string. For this task—finding the actual wrapping of text inside a DOM card—read the DOM after readiness. Treat a canvas measurement as a separate calculation with its own text settings. CanvasRenderingContext2D: measureText() method.

What should you do when measurements still change?

Use this diagnostic sequence before adding another delay:

  1. Confirm the stylesheet completion you awaited belongs to the stylesheet that styles the target. For dynamically inserted sheets, keep their loading promises with the insertion code.
  2. Inspect the target’s resolved typography and width. A loaded stylesheet can still lose in the cascade.
  3. Confirm the target text and its intended container are present before awaiting font readiness.
  4. Decide whether fallback is acceptable. If a specific declared face is required, use explicit loading and report failure.
  5. Check whether another application update changed the text, classes or container after your wait. Restart the measurement for that new state.

A frame callback is not a resource-loading barrier. requestAnimationFrame() schedules a callback before a repaint, and browsers commonly pause it in background tabs. Use it when frame scheduling serves your application, but do not replace stylesheet or font promises with one or two frame callbacks. Window: requestAnimationFrame() method.

For a resizable layout, observe the relevant container with ResizeObserver and schedule fresh measurements when its dimensions change. The observer reports content-box or border-box size changes; it is not a universal text-change notification. Also trigger measurement from the application operations that replace text or change typography. ResizeObserver.

If you already use Playwright, perform the browser-side readiness wait and geometry reads inside an asynchronous page.evaluate() callback. Playwright waits for its returned promise and brings the result back to the calling environment. Return plain numeric rectangle fields, as the example does, and pass any caller-side values explicitly into the callback. Evaluating JavaScript.

A useful measurement belongs to a particular text, stylesheet state, font outcome and container size. Establish those inputs, await the relevant loading work, and measure the intact DOM. Repeat when those inputs change so that the recorded geometry continues to answer the layout question you actually have.

Sources