Measure per-letter dimension of text in JavaScript
Tags: html, javascript
Published: 2021-02-10 01:58:15

---
title: Measure per-letter dimension of text in JavaScript
published: true
date: 2021-01-09 05:14:49 UTC
tags: HTML,JavaScript
canonical_url: https://blog.1a23.com/2021/01/09/measure-per-letter-dimension-of-text-in-javascript/?utm_source=rss&utm_medium=rss&utm_campaign=measure-per-letter-dimension-of-text-in-javascript
cover_image: https://blog.1a23.com/wp-content/uploads/sites/2/2021/01/text-dimension-1568x1176.png
---

> **TL;DR**: Create a [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range), set proper start and end points up to the text node with proper offset, then use `Range.getBoundingClientRect()` to get the dimensions.

As a part of _[Lyricova Jukebox](https://github.com/blueset/project-lyricova/tree/master/packages/jukebox)_, we wanted to support inline karaoke swipe animation. With the time tags in the data, it is easy to figure out the time when the animation must reach a certain character. Then we need to figure out a way measure per character dimension for the animation to work.

Initially, I used to do in the way [BalanceText](https://github.com/adobe/balance-text/) does, modifying DOM content and CSS properties to measure the DOM node itself. BalanceText is a JavaScript library I recently read on, it inserts `<br>` tags in to text in order to achieve a balanced line length by overriding the DOM HTML source of the targeted node directly.

This indeed could work in a way, especially for BalancedText where it operates based on the “break opportunities” within the pure HTML source. However, that brings a problem to some applications that relies on those DOM elements. Unlike BalanceText, we don’t necessary need to modify the content, especially the DOM structure, just to measure its dimensions. The way BalanceText does discards the old DOM subtree, and replaces them with a new one by directly modifying the `innerHTML` property of the subtree. This means any event listeners and other references to the element are all unusable after the measurement. This is certainly something that we don’t want.

## The Range API

After some digging through the internet, I found this [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) API of the web, which can select a region of the webpage and measure its dimension. This API is used to represent [selections by users](https://developer.mozilla.org/en-US/docs/Web/API/Selection) in a webpage, but it isn’t necessary to modify the selection in order to use it.

In this way, we can pretty much say that we can measure the per-character width of text on the webpage without any modification to the DOM tree or even the UI.

I will demonstrate this feature by measuring an accumulating width of the text per character, i.e. the width of text from the beginning to the end of the _n_th character for all _n_.

The general idea is to:

1. Locate the starting point. In this example we want to measure from the beginning of a node, so we can just use the `Range. __proto__.setStartBefore(el)` method.
2. Recursively locate all text nodes in the sub-DOM tree.
3. For each text node found
  1. Set the range to end on each character of the node by using `Range. __proto__.setEnd(el, count)`.
  2. Get the width of the range with `Range. __proto__.getBoundingClientRect()`.

Below is a sample code for the process in TypeScript.

```typescript
/**
 * A generator that recursively find all text nodes in a subtree rooted at el.
 */
function* recursivelyFindTextNode(el: Node): Generator<Node> {
  if (el.nodeType === Node.TEXT_NODE) {
    yield el;
  } else if (el.nodeType === Node.ELEMENT_NODE) {
    for (let i = 0; i < el.childNodes.length; i++) {
      yield * recursivelyFindTextNode(el.childNodes[i]);
    }
  }
}

const result: number[] = [];
const range = document.createRange();
range.setStartBefore(el);

// expand all text nodes found.
const nodes = [...recursivelyFindTextNode(el)];

for (const textNode of nodes) {
const textLength = [...textNode.textContent].length;
  for (let i = 0; i < textLength; i++) {
    range.setEnd(textNode, i);
    const rect = range.getBoundingClientRect();
    if (rect.width !== 0) result.push(rect.width);
  }
}

// Get the total length of the text
range.setEndAfter(nodes.length > 0 ? nodes[nodes.length - 1] : el);
const rect = range.getBoundingClientRect();
result.push(rect.width);
```

## Caveat

Since this API is measuring things rendered on screen directly, if you want to measure something, it has to be rendered first.

Also, if you want to measure the length of a long string of text in pixels, the text must be rendered in one line. This is achievable by a few lines of CSS:

```css
white-space: pre;
display: inline;
width: fit-content;
```

* * *

Finally, here is a simple CodePen demo:

{% codepen https://codepen.io/blueset/pen/rNMrdNO default-tab=result %}

The post [Measure per-letter dimension of text in JavaScript](https://blog.1a23.com/2021/01/09/measure-per-letter-dimension-of-text-in-javascript/) appeared first on [1A23 Blog](https://blog.1a23.com).