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).
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
const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
const context = canvas.getContext("2d");
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
}
function getCssStyle(element, prop) {
return window.getComputedStyle(element, null).getPropertyValue(prop);
}
function getCanvasFont(el = document.body) {
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
const fontSize = getCssStyle(el, 'font-size') || '16px';
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
return `${fontWeight} ${fontSize} ${fontFamily}`;
}
console.log(getTextWidth("hello there!", "bold 12pt arial")); // close to 86
If you want to use the font-size of some specific element myEl, you can make use of the getCanvasFont utility function:
const fontSize = getTextWidth(text, getCanvasFont(myEl));
// do something with fontSize here...
Explanation: The getCanvasFontSize function takes some element's (by default: the body's) font and converts it into a format compatible with the Context.font property. Of course any element must first be added to the DOM before usage, else it gives you bogus values.
More Notes
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).
Performance
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.
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>
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.
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.
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.textContent = pText;
var lResult = {
width: lDiv.clientWidth,
height: lDiv.clientHeight
};
document.body.removeChild(lDiv);
lDiv = null;
return lResult;
}
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 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
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/
<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.
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;
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;
}
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)
}
}
});
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) {
...
}
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>
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
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;
}
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
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.
(function($) {
$.format = function(format) {
return (function(format, args) {
return format.replace(/{(\d+)}/g, function(val, pos) {
return typeof args[pos] !== 'undefined' ? args[pos] : val;
});
}(format, [].slice.call(arguments, 1)));
};
$.measureText = function(html, fontOptions) {
fontOptions = $.extend({
fontSize: '1em',
fontStyle: 'normal',
fontWeight: 'normal',
fontFamily: 'arial'
}, fontOptions);
var $el = $('<div>', {
html: html,
css: {
position: 'absolute',
left: -1000,
top: -1000,
display: 'none'
}
}).appendTo('body');
$(fontOptions).each(function(index, option) {
$el.css(option, fontOptions[option]);
});
var h = $el.outerHeight(), w = $el.outerWidth();
$el.remove();
return { height: h, width: w };
};
}(jQuery));
var dimensions = $.measureText("Hello World!", { fontWeight: 'bold', fontFamily: 'arial' });
// Font Dimensions: 94px x 18px
$('body').append('<p>').text($.format('Font Dimensions: {0}px x {1}px', dimensions.width, dimensions.height));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
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>
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>
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
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'));
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");
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);
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");
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