Calculate text width with JavaScript

Viewed 539370

I'd like to use JavaScript to calculate the width of a string. Is this possible without having to use a monospace typeface?

If it's not built-in, my only idea is to create a table of widths for each character, but this is pretty unreasonable especially supporting Unicode and different type sizes (and all browsers for that matter).

29 Answers

Create a DIV styled with the following styles. In your JavaScript, set the font size and attributes that you are trying to measure, put your string in the DIV, then read the current width and height of the DIV. It will stretch to fit the contents and the size will be within a few pixels of the string rendered size.

var fontSize = 12;
var test = document.getElementById("Test");
test.style.fontSize = fontSize;
var height = (test.clientHeight + 1) + "px";
var width = (test.clientWidth + 1) + "px"

console.log(height, width);
#Test
{
    position: absolute;
    visibility: hidden;
    height: auto;
    width: auto;
    white-space: nowrap; /* Thanks to Herb Caudill comment */
}
<div id="Test">
    abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
</div>

jQuery:

(function($) {

 $.textMetrics = function(el) {

  var h = 0, w = 0;

  var div = document.createElement('div');
  document.body.appendChild(div);
  $(div).css({
   position: 'absolute',
   left: -1000,
   top: -1000,
   display: 'none'
  });

  $(div).html($(el).html());
  var styles = ['font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing'];
  $(styles).each(function() {
   var s = this.toString();
   $(div).css(s, $(el).css(s));
  });

  h = $(div).outerHeight();
  w = $(div).outerWidth();

  $(div).remove();

  var ret = {
   height: h,
   width: w
  };

  return ret;
 }

})(jQuery);

I like your "only idea" of just doing a static character width map! It actually works well for my purposes. Sometimes, for performance reasons or because you don't have easy access to a DOM, you may just want a quick hacky standalone calculator calibrated to a single font. So here's one calibrated to Helvetica; pass a string and a font size:

const widths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.2796875,0.2765625,0.3546875,0.5546875,0.5546875,0.8890625,0.665625,0.190625,0.3328125,0.3328125,0.3890625,0.5828125,0.2765625,0.3328125,0.2765625,0.3015625,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.2765625,0.2765625,0.584375,0.5828125,0.584375,0.5546875,1.0140625,0.665625,0.665625,0.721875,0.721875,0.665625,0.609375,0.7765625,0.721875,0.2765625,0.5,0.665625,0.5546875,0.8328125,0.721875,0.7765625,0.665625,0.7765625,0.721875,0.665625,0.609375,0.721875,0.665625,0.94375,0.665625,0.665625,0.609375,0.2765625,0.3546875,0.2765625,0.4765625,0.5546875,0.3328125,0.5546875,0.5546875,0.5,0.5546875,0.5546875,0.2765625,0.5546875,0.5546875,0.221875,0.240625,0.5,0.221875,0.8328125,0.5546875,0.5546875,0.5546875,0.5546875,0.3328125,0.5,0.2765625,0.5546875,0.5,0.721875,0.5,0.5,0.5,0.3546875,0.259375,0.353125,0.5890625]
const avg = 0.5279276315789471

function measureText(str, fontSize) {
  return Array.from(str).reduce(
    (acc, cur) => acc + (widths[cur.charCodeAt(0)] ?? avg), 0
  ) * fontSize
}

That giant ugly array is ASCII character widths indexed by character code. So this just supports ASCII (otherwise it assumes an average character width). Fortunately, width basically scales linearly with font size, so it works pretty well at any font size. It's noticeably lacking any awareness of kerning or ligatures or whatever.

To "calibrate" I just rendered every character up to charCode 126 (the mighty tilde) on an svg and got the bounding box and saved it to this array; more code and explanation and demo here.

<span id="text">Text</span>

<script>
var textWidth = document.getElementById("text").offsetWidth;
</script>

This should work as long as the <span> tag has no other styles applied to it. offsetWidth will include the width of any borders, horizontal padding, vertical scrollbar width, etc.

In case anyone else got here looking both for a way to measure the width of a string and a way to know what's the largest font size that will fit in a particular width, here is a function that builds on @Domi's solution with a binary search:

/**
 * Find the largest font size (in pixels) that allows the string to fit in the given width.
 * 
 * @param {String} text - The text to be rendered.
 * @param {String} font - The css font descriptor that text is to be rendered with (e.g. "bold ?px verdana") -- note the use of ? in place of the font size.
 * @param {Number} width - The width in pixels the string must fit in
 * @param {Number} minFontPx - The smallest acceptable font size in pixels
 * @param {Number} maxFontPx - The largest acceptable font size in pixels
 **/
function GetTextSizeForWidth(text, font, width, minFontPx, maxFontPx) {
  for (;;) {
    var s = font.replace("?", maxFontPx);
    var w = GetTextWidth(text, s);
    if (w <= width) {
      return maxFontPx;
    }

    var g = (minFontPx + maxFontPx) / 2;

    if (Math.round(g) == Math.round(minFontPx) || Math.round(g) == Math.round(maxFontPx)) {
      return g;
    }

    s = font.replace("?", g);
    w = GetTextWidth(text, s);
    if (w >= width) {
      maxFontPx = g;
    } else {
      minFontPx = g;
    }
  }
}

You can also do this with createRange, which is more accurate, than the text cloning technique:

function getNodeTextWidth(nodeWithText) {
    var textNode = $(nodeWithText).contents().filter(function () {
        return this.nodeType == Node.TEXT_NODE;
    })[0];
    var range = document.createRange();
    range.selectNode(textNode);
    return range.getBoundingClientRect().width;
}

If you're okay with installing a package, and you want perhaps a more authoritative or precise answer, you can use opentype.js (surprised no one has mentioned this yet):

import { load } from "opentype.js";

const getWidth = async (text = "Hello World") => {
  const font = await load("path/to/some/font");
  const { x1, x2 } = font.getPath(text, 0, 0, 12).getBoundingBox();
  return x2 - x1;
};

Naturally you'd want to only call load once per font, so you should pull that line out to a higher scope based on your circumstances.

Here's a Code Sandbox comparing this OpenType method to the Canvas and DOM methods: https://codesandbox.io/s/measure-width-of-text-in-javascript-vctst2

On my machine, for 100 samples each, the typical results are:

  • OpenType: 5ms
  • Canvas: 3ms
  • DOM: 4ms

Another package I found is this one: https://github.com/sffc/word-wrappr

Rewritten my answer from scratch (thanks for that minus). Now function accepts a text and css rules to be applied (and doesn't use jQuery anymore). So it will respect paddings too. Resulting values are being rounded (you can see Math.round there, remove if you want more that precise values)

function getSpan(){
    const span = document.createElement('span')
    span.style.position = 'fixed';
    span.style.visibility = 'hidden';
    document.body.appendChild(span);
    return span;
}

function textWidth(str, css) {
    const span = getSpan();
    Object.assign(span.style, css || {});
    span.innerText = str;
    const w = Math.round(span.getBoundingClientRect().width);
    
    span.remove();
    
    return w;
}


const testStyles = [
  {fontSize: '10px'},
  {fontSize: '12px'},
  {fontSize: '60px'},
  {fontSize: '120px'},
  {fontSize: '120px', padding: '10px'},
  {fontSize: '120px', fontFamily: 'arial'},
  {fontSize: '120px', fontFamily: 'tahoma'},
  {fontSize: '120px', fontFamily: 'tahoma', padding: '5px'},
];

const ul = document.getElementById('output');
testStyles.forEach(style => {
  const li = document.createElement('li');
  li.innerText = `${JSON.stringify(style)} > ${textWidth('abc', style)}`;
  ul.appendChild(li);
});
<ul id="output"></ul>

You can use max-content to measure the pixel width of text.

Here is a utility function that does that. It optionally takes any node as a context to calculate the width in, taking into account any CSS like font-size, letter-spacing, etc.

function measureTextPxWidth(
  text,
  template = document.createElement("span")
) {
  const measurer = template.cloneNode();
  measurer.style.setProperty("all", "revert", "important");
  measurer.style.setProperty("position", "position", "important");
  measurer.style.setProperty("visibility", "hidden", "important");
  measurer.style.setProperty("width", "max-content", "important");
  measurer.innerText = text;

  document.body.appendChild(measurer);
  const { width } = measurer.getBoundingClientRect();
  document.body.removeChild(measurer);
  return width;
}

document.querySelector('.spanTextWidth').innerText = 
  `${measureTextPxWidth('one two three')}px`
  
document.querySelector('.h1TextWidth').innerText = 
  `${measureTextPxWidth('one two three', document.querySelector('h1'))}px`
h1 {
  letter-spacing: 3px;
}
<span>one two three</span>
<div class="spanTextWidth"></div>
  
<h1>one two three</h1>
<div class="h1TextWidth"></div>

For any one out there using React and/or Typescript...

Try this Codepen!

export default function App() {
  const spanRef = useRef<HTMLSpanElement>(null);
  const [textWidth, setTextWidth] = useState(0);

  const getTextWidthInPixels = (ref: HTMLSpanElement) =>
    ref.getBoundingClientRect().width;

  useEffect(() => {
    setTextWidth(getTextWidthInPixels(spanRef.current!));
  }, [spanRef]);

  return (
    <div className="App">
      <span
        ref={spanRef}
        contentEditable
        suppressContentEditableWarning
        onInput={() => setTextWidth(getTextWidthInPixels(spanRef.current!))}
      >
        Edit Me!!!
      </span>
      {`textWidth: ${textWidth}px`}
    </div>
  );
}

  • It's a good idea to wrap our text in an inline-positioned element (like a <span>)
  • useRef is the React way to access a DOM element, the <span> in our case
  • getBoundingClientRect can get the total width of any DOM element.
  • contentEditable allows users to change the contents of an element ...which is a little unsafe (React will throw warnings!)
  • suppressContentEditableWarning will help us prevent these warnings

The Element.getClientRects() method returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client. The returned value is a collection of DOMRect objects, one for each CSS border box associated with the element. Each DOMRect object contains read-only left, top, right and bottom properties describing the border box, in pixels, with the top-left relative to the top-left of the viewport.

Element.getClientRects() by Mozilla Contributors is licensed under CC-BY-SA 2.5.

Summing up all returned rectangle widths yields the total text width in pixels.

document.getElementById('in').addEventListener('input', function (event) {
    var span = document.getElementById('text-render')
    span.innerText = event.target.value
    var rects = span.getClientRects()
    var widthSum = 0
    for (var i = 0; i < rects.length; i++) {
        widthSum += rects[i].right - rects[i].left
    }
    document.getElementById('width-sum').value = widthSum
})
<p><textarea id='in'></textarea></p>
<p><span id='text-render'></span></p>
<p>Sum of all widths: <output id='width-sum'>0</output>px</p>

I'm using text-metrics package. Works really nice, I tried this solution but in some reasons, it counts it wrong.

textMetrics.init(document.querySelector('h1'), { fontSize: '20px' });

textMetrics.init({
  fontSize: '14px',
  lineHeight: '20px',
  fontFamily: 'Helvetica, Arial, sans-serif',
  fontWeight: 400,
  width: 100,
});

Hey Everyone I know I'm a little late to the party but here we go

window.addEventListener("error",function(e){ alert(e.message); });
var canvas = new OffscreenCanvas(400, 50);
var ctx = canvas.getContext("2d");
ctx.font = "16px Ariel"; //this can be dynamic using getComputedStyle
const chars = ["a","b","c","d","e","f"," ","    "];
const charWidths = new Map();
while(chars.length > 0){
  var char = chars.shift();
  var wide = ctx.measureText(char).width;
  charWidths.set(char,wide);
}

and then you can use it with something like:

var pixelWidth = charWidths.get("0");
//fyi css properties like letter-spacing need to be accounted for

Use scrollWidth on the containing element of the text to get the minimum width of the element including hidden parts due to overflow. More information at https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth

If the element is not in the DOM, add it to some hidden area to do the measurement. For example:

function measureText(text) {
  let div = document.createElement("div");
  div.innerText = text;
  div.style.whiteSpace = 'nowrap';
  body.appendChild(div);
  let width = div.scrollWidth;
  body.removeChild(div);
  return width;
}

The style (font-size, weight, etc.) will be inherited by the element and thus accounted in the width. You could also measure the size of more complex content with scrollWidth and scrollHeight.

var textWidth = (function (el) {
    el.style.position = 'absolute';
    el.style.top = '-1000px';
    document.body.appendChild(el);

    return function (text) {
        el.innerHTML = text;
        return el.clientWidth;
    };
})(document.createElement('div'));
Related