Converting em to px in Javascript (and getting default font size)

JavascriptCssEventsDom

Javascript Problem Overview


There are situations where it can be useful to get the exact pixel width of an em measurement. For example, suppose you have an element with a CSS property (like border or padding) which is measured in ems, and you need to get the exact pixel width of the border or padding. There's an existing question which touches on this topic:

https://stackoverflow.com/questions/1442542/how-can-i-get-default-font-size-in-pixels-by-using-javascript-or-jquery

This question is asking about getting the default font size - which is necessary in order to convert a relative em value to an exact px value.

This answer has a pretty good explanation of how to go about getting the default font size of an element:

> Since ems measure width you can always compute the exact pixel font > size by creating a div that is 1000 ems long and dividing its > client-Width property by 1000. I seem to recall ems are truncated to > the nearest thousandth, so you need 1000 ems to avoid an erroneous > truncation of the pixel result.

So, using this answer as a guide, I wrote the following function to get the default font size:

function getDefaultFontSize(parentElement)
{
	parentElement = parentElement || document.body;
	var div = document.createElement('div');
	div.style.width = "1000em";
	parentElement.appendChild(div);
  	var pixels = div.offsetWidth / 1000;
	parentElement.removeChild(div);
	return pixels;
}

Once you have the default font size, you can convert any em width to px width by just multiplying the ems by the element's default font size and rounding the result:

Math.round(ems * getDefaultFontSize(element.parentNode))

Problem: The getDefaultFontSize is ideally supposed to be a simple, side-effect free function that returns the default font size. But it DOES have an unfortunate side effect: it modifies the DOM! It appends a child and then removes the child. Appending the child is necessary in order to get a valid offsetWidth property. If you don't append the child div to the DOM, the offsetWidth property remains at 0 because the element is never rendered. Even though we immediately remove the child element, this function can still create unintended side effects, such as firing an event (like Internet Explorer's onpropertychange or W3C's DOMSubtreeModified event) that was listening on the parent element.

Question: Is there any way to write a truly side-effect free getDefaultFontSize() function that won't accidentally fire event handlers or cause other unintended side effects?

Javascript Solutions


Solution 1 - Javascript

Edit: No, there isn't.

To get the rendered font size of a given element, without affecting the DOM:

parseFloat(getComputedStyle(parentElement).fontSize);

This is based off the answer to this question.


Edit:

In IE, you would have to use parentElement.currentStyle["fontSize"], but this is not guaranteed to convert the size to px. So that's out.

Furthermore, this snippet won't get you the default font size of the element, but rather its actual font size, which is important if it has actually got a class and a style associated with it. In other words, if the element's font size is 2em, you'll get the number of pixels in 2 ems. Unless the font size is specified inline, you won't be able to get the conversion ratio right.

Solution 2 - Javascript

I have a better answer. My code will store the length of 1em (in CSS pixel px units in the JavaScript variable em:

  1. Place this div anywhere in your HTML code

    <div id="div" style="height:0;width:0;outline:none;border:none;padding:none;margin:none;box-sizing:content-box;"></div>
    
  2. Place this function in your JavaScript file

    var em;
    function getValue(id){
        var div = document.getElementById(id);
        div.style.height = '1em';
        return ( em = div.offsetHeight );
    }
    

Now, whenever you will call this function 'getValue' with id of that test div in parameter, you will have a variable name em which will contain value of 1 em in px.

Solution 3 - Javascript

If you need something quick and dirty (and based on base font-size of body, not an element), I'd go with:

Number(getComputedStyle(document.body,null).fontSize.replace(/[^\d]/g, ''))

 Number(  // Casts numeric strings to number
   getComputedStyle(  // takes element and returns CSSStyleDeclaration object
     document.body,null) // use document.body to get first "styled" element
         .fontSize  // get fontSize property
          .replace(/[^\d]/g, '')  // simple regex that will strip out non-numbers
 ) // returns number

Solution 4 - Javascript

The following solution uses binary search and window.matchMedia to find out the em width of a window. We then divide the window em width with the window pixel width to get the final result. No DOM involved, side-effects free! In terms of performance it takes around 8 iterations at around 0.2ms on my comp.

const singleEmInPixels = getSingleEmInPixels();
console.log(`1em = ${singleEmInPixels}px`);

/**
 * returns the amount of pixels for a single em
 */
function getSingleEmInPixels() {
    let low = 0;
    let high = 200;
    let emWidth = Math.round((high - low) / 2) + low;
    const time = performance.now();
    let iters = 0;
    const maxIters = 10;
    while (high - low > 1) {
        const match = window.matchMedia(`(min-width: ${emWidth}em)`).matches;
        iters += 1;
        if (match) {
            low = emWidth;
        } else {
            high = emWidth;
        }
        emWidth = Math.round((high - low) / 2) + low;
        if (iters > maxIters) {
            console.warn(`max iterations reached ${iters}`);
            break;
        }
    }
    const singleEmPx = Math.ceil(window.innerWidth / emWidth);
    console.log(`window em width = ${emWidth}, time elapsed =  ${(performance.now() - time)}ms`);
    return singleEmPx;
}

Solution 5 - Javascript

I needed the em size and this was where I landed. I was inspired by @10011101111's solution to write this general solution. Note, that this may not necessarily be without side-effects as the OP requires, but it may be possible to adapt this to have minimal side effects.

This solution is for those landing here with the same question as I had.

To get a whole-pixel result:

function getSize(size = '1em', parent = document.body) {
	let l = document.createElement('div')
	l.style.visibility = 'hidden'
	l.style.boxSize = 'content-box'
	l.style.position = 'absolute'
	l.style.maxHeight = 'none'
	l.style.height = size
	parent.appendChild(l)
	size = l.clientHeight
	l.remove()
	return size
}

To get a more precise result:

function getSizePrecise(size = '1em', parent = document.body) {
	let l = document.createElement('div'), i = 1, s, t
	l.style.visibility = 'hidden'
	l.style.boxSize = 'content-box'
	l.style.position = 'absolute'
	l.style.maxHeight = 'none'
	l.style.height = size
	parent.appendChild(l)
	t = l.clientHeight
	do {
		s = t
		i *= 10
		l.style.height = 'calc(' + i + '*' + size + ')'
		t = l.clientHeight
	} while(t !== s * 10)
	l.remove()
	return t / i
}

Note that you can pass in any size of any unit. For example:

getSizePrecise('1ex')
getSizePrecise('1.111ex')

You may also be able to adapt this for CSS < 3 compatibility by removing the calc() function and directly multiplying the size value except the unit by the multiplier in JavaScript.

If the value overflows, this will probably return 0.

In trying to figure out the overflow limit, I reached the pixel value of 17895696 or 17895697 on Firefox 88.0. So, I updated the above function as follows:

function getSizePrecise(size = '1em', parent = document.body) {
	let l = document.createElement('div'), i = 1, s, t
	l.style.visibility = 'hidden'
	l.style.boxSize = 'content-box'
	l.style.position = 'absolute'
	l.style.maxHeight = 'none'
	l.style.height = size
	parent.appendChild(l)
	t = l.clientHeight
	do {
		if (t > 1789569.6)
			break
		s = t
		i *= 10
		l.style.height = 'calc(' + i + '*' + size + ')'
		t = l.clientHeight
	} while(t !== s * 10)
	l.remove()
	return t / i
}

The .6 is strictly not necessary (as t will probably be a whole value), but I'm leaving it there for reference.

Solution 6 - Javascript

The following JS function will convert any CSS Length Unit Value to another.
Cheers! 

// Usage examples: convertCSSLengthUnit('26.556016597510375mm'); convertCSSLengthUnit('26.556016597510375mm', 'px'); convertCSSLengthUnit('100px', 'mm');
function convertCSSLengthUnit(fromUnitValue, toUnit){
  let value = fromUnitValue.match(/[0-9]+\.*[0-9]*/g)[0];
  let unit = fromUnitValue.match(/[a-zA-Z]+/g)[0];

  let frag = document.createRange().createContextualFragment(`
    <div style='all: initial; pointer-events: none; display: block; position: absolute; border: none; padding: 0; margin: 0; background: rgba(0,0,0,0); color: color: rgba(0,0,0,0); width: 1${unit}; height: 1px;'></div>
  `);
  document.body.appendChild(frag);
  let measureElement = document.body.children[document.body.children.length-1];
  let toUnitValuePixel = measureElement.getBoundingClientRect().width * value; // X
  measureElement.remove();

  if(toUnit){
    let frag = document.createRange().createContextualFragment(`
      <div style='all: initial; pointer-events: none; display: block; position: absolute; border: none; padding: 0; margin: 0; background: rgba(0,0,0,0); color: color: rgba(0,0,0,0); width: 1${toUnit}; height: 1px;'></div>
    `);
    document.body.appendChild(frag);
    let measureElement = document.body.children[document.body.children.length-1];
    let valueUnit = measureElement.getBoundingClientRect().width; // Y
    measureElement.remove();

    // Given: Aem and Bmm with B=1. We know: Aem = Xpx and Bmm = Ypx. Therefore: 1px = Bmm/Y Result: Aem = Xpx = X * 1px = X * Bmm/Y <=> Aem = X * 1mm/Y (with B=1) <=> Aem = X/Ymm
    return (toUnitValuePixel / valueUnit) + toUnit;
  }
  return toUnitValuePixel + 'px';
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionChannel72View Question on Stackoverflow
Solution 1 - JavascriptmgiuffridaView Answer on Stackoverflow
Solution 2 - Javascript10011101111View Answer on Stackoverflow
Solution 3 - JavascriptAnthonyView Answer on Stackoverflow
Solution 4 - JavascriptNadav ParagView Answer on Stackoverflow
Solution 5 - JavascriptdVVIIbView Answer on Stackoverflow
Solution 6 - JavascriptSYNAIKIDOView Answer on Stackoverflow