[javascript] Calculate text width with JavaScript

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).

This question is related to javascript textbox

The answer is


In HTML 5, you can just use the Canvas.measureText method (further explanation here).

Try this fiddle:

/**
 * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
 * 
 * @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 14px verdana").
 * 
 * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
 */
function getTextWidth(text, font) {
    // re-use canvas object for better performance
    var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
    var context = canvas.getContext("2d");
    context.font = font;
    var metrics = context.measureText(text);
    return metrics.width;
}

console.log(getTextWidth("hello there!", "bold 12pt arial"));  // close to 86

This fiddle compares this Canvas method to a variation of Bob Monteverde's DOM-based method, so you can analyze and compare accuracy of the results.

There are several advantages to this approach, including:

  • More concise and safer than the other (DOM-based) methods because it does not change global state, such as your DOM.
  • Further customization is possible by modifying more canvas text properties, such as textAlign and textBaseline.

NOTE: When you add the text to your DOM, remember to also take account of padding, margin and border.

NOTE 2: On some browsers, this method yields sub-pixel accuracy (result is a floating point number), on others it does not (result is only an int). You might want to run Math.floor (or Math.ceil) on the result, to avoid inconsistencies. Since the DOM-based method is never sub-pixel accurate, this method has even higher precision than the other methods here.

According to this jsperf (thanks to the contributors in comments), the Canvas method and the DOM-based method are about equally fast, if caching is added to the DOM-based method and you are not using Firefox. In Firefox, for some reason, this Canvas method is much much faster than the DOM-based method (as of September 2014).


I wrote a little tool for that. Perhaps it's useful to somebody. It works without jQuery.

https://github.com/schickling/calculate-size

Usage:

var size = calculateSize("Hello world!", {
   font: 'Arial',
   fontSize: '12px'
});

console.log(size.width); // 65
console.log(size.height); // 14

Fiddle: http://jsfiddle.net/PEvL8/


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;
}

I guess this is prety similar to Depak entry, but is based on the work of Louis Lazaris published at an article in impressivewebs page

(function($){

        $.fn.autofit = function() {             

            var hiddenDiv = $(document.createElement('div')),
            content = null;

            hiddenDiv.css('display','none');

            $('body').append(hiddenDiv);

            $(this).bind('fit keyup keydown blur update focus',function () {
                content = $(this).val();

                content = content.replace(/\n/g, '<br>');
                hiddenDiv.html(content);

                $(this).css('width', hiddenDiv.width());

            });

            return this;

        };
    })(jQuery);

The fit event is used to execute the function call inmediatly after the function is asociated to the control.

e.g.: $('input').autofit().trigger("fit");


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;
    }
  }
}

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 (optionally) a font size:

function measureText(str, fontSize = 10) {
  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
  return str
    .split('')
    .map(c => c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg)
    .reduce((cur, acc) => acc + cur) * 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.


Fiddle of working example: http://jsfiddle.net/tdpLdqpo/1/

HTML:

<h1 id="test1">
    How wide is this text?
</h1>
<div id="result1"></div>
<hr/>
<p id="test2">
    How wide is this text?
</p>
<div id="result2"></div>
<hr/>
<p id="test3">
    How wide is this text?<br/><br/>
    f sdfj f sdlfj lfj lsdk jflsjd fljsd flj sflj sldfj lsdfjlsdjkf sfjoifoewj flsdjfl jofjlgjdlsfjsdofjisdojfsdmfnnfoisjfoi  ojfo dsjfo jdsofjsodnfo sjfoj ifjjfoewj fofew jfos fojo foew jofj s f j
</p>
<div id="result3"></div>

JavaScript code:

function getTextWidth(text, font) {
    var canvas = getTextWidth.canvas ||
        (getTextWidth.canvas = document.createElement("canvas"));
    var context = canvas.getContext("2d");
    context.font = font;
    var metrics = context.measureText(text);
    return metrics.width;
};

$("#result1")
.text("answer: " +
    getTextWidth(
             $("#test1").text(),
             $("#test1").css("font")) + " px");

$("#result2")
    .text("answer: " +
        getTextWidth(
             $("#test2").text(),
             $("#test2").css("font")) + " px");

$("#result3")
    .text("answer: " +
        getTextWidth(
             $("#test3").text(),
             $("#test3").css("font")) + " px");

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)

_x000D_
_x000D_
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);
});
_x000D_
<ul id="output"></ul>
_x000D_
_x000D_
_x000D_


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);

The code-snips below, "calculate" the width of the span-tag, appends "..." to it if its too long and reduces the text-length, until it fits in its parent (or until it has tried more than a thousand times)

CSS

div.places {
  width : 100px;
}
div.places span {
  white-space:nowrap;
  overflow:hidden;
}

HTML

<div class="places">
  <span>This is my house</span>
</div>
<div class="places">
  <span>And my house are your house</span>
</div>
<div class="places">
  <span>This placename is most certainly too wide to fit</span>
</div>

JavaScript (with jQuery)

// loops elements classed "places" and checks if their child "span" is too long to fit
$(".places").each(function (index, item) {
    var obj = $(item).find("span");
    if (obj.length) {
        var placename = $(obj).text();
        if ($(obj).width() > $(item).width() && placename.trim().length > 0) {
            var limit = 0;
            do {
                limit++;
                                    placename = placename.substring(0, placename.length - 1);
                                    $(obj).text(placename + "...");
            } while ($(obj).width() > $(item).width() && limit < 1000)
        }
    }
});

Here's one I whipped together without example. It looks like we are all on the same page.

String.prototype.width = function(font) {
  var f = font || '12px arial',
      o = $('<div></div>')
            .text(this)
            .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden', 'font': f})
            .appendTo($('body')),
      w = o.width();

  o.remove();

  return w;
}

Using it is simple: "a string".width()

**Added white-space: nowrap so strings with width larger than the window width can be calculated.


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'));

The ExtJS javascript library has a great class called Ext.util.TextMetrics that "provides precise pixel measurements for blocks of text so that you can determine exactly how high and wide, in pixels, a given block of text will be". You can either use it directly or view its source to code to see how this is done.

http://docs.sencha.com/extjs/6.5.3/modern/Ext.util.TextMetrics.html


This works for me...

// Handy JavaScript to measure the size taken to render the supplied text;
// you can supply additional style information too if you have it.

function measureText(pText, pFontSize, pStyle) {
    var lDiv = document.createElement('div');

    document.body.appendChild(lDiv);

    if (pStyle != null) {
        lDiv.style = pStyle;
    }
    lDiv.style.fontSize = "" + pFontSize + "px";
    lDiv.style.position = "absolute";
    lDiv.style.left = -1000;
    lDiv.style.top = -1000;

    lDiv.innerHTML = pText;

    var lResult = {
        width: lDiv.clientWidth,
        height: lDiv.clientHeight
    };

    document.body.removeChild(lDiv);
    lDiv = null;

    return lResult;
}

Try this code:

function GetTextRectToPixels(obj)
{
var tmpRect = obj.getBoundingClientRect();
obj.style.width = "auto"; 
obj.style.height = "auto"; 
var Ret = obj.getBoundingClientRect(); 
obj.style.width = (tmpRect.right - tmpRect.left).toString() + "px";
obj.style.height = (tmpRect.bottom - tmpRect.top).toString() + "px"; 
return Ret;
}

You can use the canvas so you don't have to deal so much with css properties:

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.font = "20pt Arial";  // This can be set programmaticly from the element's font-style if desired
var textWidth = ctx.measureText($("#myElement").text()).width;

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,
});

The better of is to detect whether text will fits right before you display the element. So you can use this function which doesn't requires the element to be on screen.

function textWidth(text, fontProp) {
    var tag = document.createElement("div");
    tag.style.position = "absolute";
    tag.style.left = "-999em";
    tag.style.whiteSpace = "nowrap";
    tag.style.font = fontProp;
    tag.innerHTML = text;

    document.body.appendChild(tag);

    var result = tag.clientWidth;

    document.body.removeChild(tag);

    return result;
}

Usage:

if ( textWidth("Text", "bold 13px Verdana") > elementWidth) {
    ...
}

Without jQuery:

String.prototype.width = function (fontSize) {
    var el,
        f = fontSize + " px arial" || '12px arial';
    el = document.createElement('div');
    el.style.position = 'absolute';
    el.style.float = "left";
    el.style.whiteSpace = 'nowrap';
    el.style.visibility = 'hidden';
    el.style.font = f;
    el.innerHTML = this;
    el = document.body.appendChild(el);
    w = el.offsetWidth;
    el.parentNode.removeChild(el);
    return w;
}

// Usage
"MyString".width(12);

<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.


The width and heigth of a text can be obtained with clientWidth and clientHeight

var element = document.getElementById ("mytext");

var width = element.clientWidth;
var height = element.clientHeight;

make sure that style position property is set to absolute

element.style.position = "absolute";

not required to be inside a div, can be inside a p or a span


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.

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


Building off of Deepak Nadar's answer, I changed the functions parameter's to accept text and font styles. You do not need to reference an element. Also, the fontOptions have defaults, so you to not need to supply all of them.

_x000D_
_x000D_
(function($) {_x000D_
  $.format = function(format) {_x000D_
    return (function(format, args) {_x000D_
      return format.replace(/{(\d+)}/g, function(val, pos) {_x000D_
        return typeof args[pos] !== 'undefined' ? args[pos] : val;_x000D_
      });_x000D_
    }(format, [].slice.call(arguments, 1)));_x000D_
  };_x000D_
  $.measureText = function(html, fontOptions) {_x000D_
    fontOptions = $.extend({_x000D_
      fontSize: '1em',_x000D_
      fontStyle: 'normal',_x000D_
      fontWeight: 'normal',_x000D_
      fontFamily: 'arial'_x000D_
    }, fontOptions);_x000D_
    var $el = $('<div>', {_x000D_
      html: html,_x000D_
      css: {_x000D_
        position: 'absolute',_x000D_
        left: -1000,_x000D_
        top: -1000,_x000D_
        display: 'none'_x000D_
      }_x000D_
    }).appendTo('body');_x000D_
    $(fontOptions).each(function(index, option) {_x000D_
      $el.css(option, fontOptions[option]);_x000D_
    });_x000D_
    var h = $el.outerHeight(), w = $el.outerWidth();_x000D_
    $el.remove();_x000D_
    return { height: h, width: w };_x000D_
  };_x000D_
}(jQuery));_x000D_
_x000D_
var dimensions = $.measureText("Hello World!", { fontWeight: 'bold', fontFamily: 'arial' });_x000D_
_x000D_
// Font Dimensions: 94px x 18px_x000D_
$('body').append('<p>').text($.format('Font Dimensions: {0}px x {1}px', dimensions.width, dimensions.height));
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
_x000D_
_x000D_
_x000D_