When creating JavaScript libraries, it's difficult to support the full range of units and most developers settle on just supporting pixels. CSS supports several units for length as explained in this MDN article but supporting them with JavaScript can be tricky. This post explains the basic technique for converting units and the inherent browser bugs that need to be overcome. This post also introduces a new Units library I've created and posted to GitHub.
If you have some weird reason to handle arbitrary units in your JavaScript library, the code demonstrated below should offer the most complete solution possible in the smallest possible package.
What We're Trying to Do
The basic goal is to create a small library that will make it possible to convert any valid value to pixels. This makes it possible to do further calculations using that pixel value.
|
The Trouble with Units
Converting between length units is not exactly straight-forward because each unit can have different pixel value based on a variety of uncontrollable conditions. Factors such as font properties (em, ch, ex), the OS settings for your display DPI (in, mm, cm, pt, pc) or even the dimensions of the browser (vh, vw, vm) can change the pixel value of a unit. Because there is no direct conversion function available to JavaScript it's necessary to rely on manipulating the style
attribute on the element in-order-to trick the browser into doing the conversion for you.
Absolute Units
The easiest units to convert are the absolute units. These are units that presumably represent real, fixed lengths (but actually don't). In the the real world there is no such thing as a relative inch — an inch is always an inch regardless of the settings on your monitor. On computers it's a little different; the absolute lengths are based on the DPI setting. On the vast majority of computers this is set to 96, which means that 1 inch usually equals 96 pixels. However, this can sometimes change: it's not entirely uncommon for the DPI to be set to 120 which would make 1 inch equal to 120 pixels. This is also already different on mobile devices like the iPhone 4 or newer (although the iPhone currently lies and claims to be 163DPI).
Because DPI is the deciding factor in absolute lengths, it's useful to define the conversions relative to the inch; this makes all of the other conversions easier (thankfully a pixel is always a pixel — except on iPhones). If you know the pixels in an inch, you can easily know the pixels in a pica.
- inches
- relative to OS DPI, usually 96px
- millimeters
- 1mm = 1in/25.4
- centimeters
- 1cm = 1in/2.54
- point
- 1pt = 1in/72
- picas
- 1pc = 1in/6
- Mozilla millimeters
- 1mozmm = whatever Mozilla feels like
Font-relative Units
As the name might suggest, font-relative units are dependent on the font settings of the element itself. The most commonly used is the em unit. One em equals the font-size
of the element (except in the case of the font-size
property where it means the font-size
of the parent element). The ex and ch units are rarely used and equal the height of lower-case "x" and the width of a "0" respectively. New to CSS3 — and much more useful than ex — is the rem unit which equals the em of the html
element. Converting rem and em to pixels is relatively straight-forward — all you really need is the element's (or the parent or html
element's) font-size
— but ex and ch require using the style
attribute as we'll see later.
Viewport-relative Units
Presumably useful for flexible layouts, these units are calculated relative to the browser dimensions and are currently only supported in IE9 and Opera. The three units are vh (viewport height/100) vw (viewport width/100) and vm (the smaller of vh or vw). These units change every time the browser is resized.
Percentage Lengths
Perhaps the most complex unit to convert is percentage. The relative length for a percentage can differ based on the CSS property but it is usually based on the width of the parent element. For instance, a width
of 50% means 50% of the width of the parent element. For things like padding
and margin
it can get confusing because these also always mean the width of the parent; counter-intuitively, padding-top
of 50% means 50% of the width of the parent element as well, not the height. The best way to figure out what percentage is referring to is to look the property up on the MDN website.
Converting Units
As mentioned above, converting units can be done reliably* by setting the value using the style
property and reading it back using getComputedStyle
**. The getComputedStyle
function always*** returns values using pixels.
*sometimes **in supported browsers ***usually
|
How Conversion Works
The code above is the bare minimum necessary to convert pixel units but it has a few problems that will be discussed later. But first, let's look at what we're doing in the code. In the the toPx
function, we can convert any unit to pixels by assigning the value to the elements style
property, reading that value back as using the curCSS
function (which uses getComputedStyle
) and then changing the element's style
back.
This method of checking a property, changing it, measuring it and changing it back is commonly referred to as the "awesome hack by Dean Edwards". (The Dean Edwards hack relies on the proprietary runtimeStyle
property because his comment was originally meant to address an IE-specific issue).
Using getComputedStyle
isn't supported in IE 8 and below; they use currentStyle
instead, and it works slightly differently. Specifically, currentStyle
won't convert anything to pixels; it just returns raw values (known as the specified value). For instance, if the fontSize
is set to 10mm, IE8 and below will return it as 10mm instead of 37.795276px. This problem is discussed in more detail below.
In the example above the default property is set to width
("the awesome hack by Dean Edwards" uses the left
property) because in non-IE browsers left
cannot be set on a position: static
element. Width will reliably return pixel results even when the element is display: inline
in any browser. It should be noted that in IE 8 and below, left
can be reliably set and retrieved on any element regardless of its position
property.
Problems with this Method
If browsers were perfect, the code above would work perfectly. However there are a number of bugs that need to be accounted for. Surprisingly, most of these bugs occur in WebKit, not IE.
WebKit and Computed Value and Used Value
WebKit will not convert percentages when they are applied to margin
, top
, bottom
, left
, or right
. There is a bug report for the position properties but it isn't clear that the WebKit team intends to fix it. The MDN page for getComputedStyle
explains that the function should report the used value and not the computed value (which is confusing to say the least). The primary difference between the two is that a computed value can sometimes be a percentage (in the case of margin
, top
, bottom
, left
, right
, text-indent
and others). WebKit is apparently using the computed value in getComputedStyle
which the bug tracker suggests is due to a disagreement about the spec. All other browsers are using the used value.
A ticket in the jQuery bug tracker proposes a fix for WebKet returning a percentage for margin. However, as noted above, this bug also applies to positioning properties as well (and text-indent
). The commit for fixing percentage margins in jQuery specifically addresses margins but it could easily be extended to support the other affected properties.
IE 8 and Below and Specified Value
IE8 and below don't support the getComputedStyle
function so the non-standard currentStyle
property must be used. Unlike getComputedStyle
, which is supposed to return the used value, currentStyle
returns the specified value (the MSDN manual calls this the "cascaded value"). The specified value is the value that should be assigned to the property but before it has been converted to an absolute unit. This is more useful than the style
property, which only exposes the inline values for the element; currentStyle
will return values specified in CSS as well as inherited values.
Thankfully IE 8 and below support a few non-standard properties that always return pixel values. The most commonly used property is pixelLeft (this is used in "the awesome hack"), however pixelWidth and pixelHeight work just as well. Opera also supports the non-standard pixel properties. WebKit supports them only for absolute units and Firefox doesn't support them at all. Interestingly, IE9 doesn't support them anymore because they were deprecated in an earlier version of IE.
Every unit (except percentages) is the same pixel value on an element no matter which property it is applied to (except fontSize
). For instance, a marginLeft
of 1em is the same length as a lineHeight
of 1em. Because of this fact, it is reasonably safe to use the special pixel values to convert units in IE 8 and below.
There is no direct way to convert the fontSize
because it could be relative to the parent elements fontSize
in the case of em and percentages. A jQuery bug about differing values for fontSize
points to a fix where using a left
of 1em will always equal the element's current fontSize
.
What runtimeStyle
Does
As noted above, "the awesome hack by Dean Edwards" uses runtimeStyle
for pixel conversions in IE. This IE-specific property is higher than the style
property in the CSS cascade which means that a runtimeStyle
value supersedes a style
value. By copying the currentStyle
value to the runtimeStyle
, "the awesome hack" avoids any potential for a FOUC. This works because changes to the style
property aren't rendered in the browser when a runtimeStyle
is set and the special pixel properties don't pay attention to the runtimeStyle
(because they belong to the style
object), so they can still be used to return the pixel value of the style
property. The runtimeStyle
is only available in IE and isn't useful for unit conversion beyond trying to fix FOUC issues if they appear in IE 8 or below.
Fixing the Bugs
Now that we understand the problems, we need to set about fixing them! The code below contains a patched curCSS
function.
|
The code above adds several fixes to the curCSS
function to correct for the issues mentioned above. It also adds a check to see if the browser is incorrectly returning the current value instead of the used value.
Changes to the curCSS Function
- Correct for IE being unable to reconcile the
fontSize
property by converting theleft
of 1em to pixels. - Check for percentage units in WebKit.
- For
top
andbottom
, compute the parent element's inner height and return the percentage. - For other properties (
margin
,left
,right
,text-indent
) convert to pixels using thewidth
property.
- For
- Correct for WebKit and Opera returning the computed value "auto" in cases
top
on an element that isposition: static
. - Correct for Firefox returning the specified value when it can't be set in cases like
top
on an element that isposition: static
.
Finishing the Job
Now that we've fixed all of the bugs with curCSS
, it's time to make toPx
a little smarter. For instance, absolute units will never change under any circumstances once they've been calculated. This means that once you know how many pixels are in an inch, converting an inch unit requires only simple algebra and you can skip "the awesome hack" altogether. The same goes for em and rem: once you know the correct fontSize
in pixels, a little algebra finishes the job, no "the awesome hack" necessary. This actually covers the majority of cases for units and leaves only percentages as the primary need for mucking with the element's style
property.
|
What Just Happened?
The final code above makes a few changes to the toPx
function as well as pre-computing all of the conversions for the absolute units.
- Pre-calculate the absolute units by converting mozmm and in to pixels using the
toPx
function and then using using the pixels-per-inch value to store the other conversions. - Look for a pre-calculated conversion or a rem or em unit to convert the value to pixels immediately.
- For rem and em, choose the correct element to get the
fontSize
from and get thefontSize
in pixels using thecurCSS
function and use that as the conversion.
What Is This Good For?
Nothing? It depends.
Anyone familiar with jQuery knows that $.css
will return the correct value for a property on an element but jQuery doesn't directly expose a method for converting units arbitrarily on an element. jQuery will convert units when calculating the start and end values for an animation (and it uses a version of "the awesome hack" to do it) but the conversion performed is limited in scope.
It's not possible to rely on jQuery (1.7.1 as of this writing) to handle unit conversion because it doesn't expose those methods to the API. Further, the optimizations for converting absolute, rem and em units without using "the awesome hack" should provide some speed improvements on any code that needs to rely on conversions. If you are writing a plugin, particularly a polyfill that requires some calculations to be done on a CSS property, properly converting units will allow users to specify values using the units that make sense to them instead of forcing everything to be specified in pixels.
JavaScript plug-ins don't usually do full-featured unit conversions because the code is typically too large to justify inclusion. However, the Final.js file above will minify to around 1.24KB (774 bytes gzipped) using YUI Compressor, 1.2KB (758 bytes gzipped) using uglify.js or 1.16KB (745 bytes gzipped) using Google Closure Compiler. The Google Closure Compiler minified version can be downloaded here.
The GitHub repo for Length and Angle units will hold the latest code and also contains a straight-forward Angle conversion library.