REM to PX Browser Function with Sass

By  on  

Performance is a key skill for front-end developers today. New CSS3 and HTML5 features help improve our apps but sometimes these features aren't well supported. This is where Graceful Degradation comes in. You want to leverage the extended features of new browsers but can't afford to ignore support for legacy browsers. I recently started a new project where IE8 support was required. Due to cascade problems when using nested 'EM' units, I decided to start my project using the useful 'REM' units, which are easier to understand and maintain. The main problem with this approach is that IE8- doesn't include support for 'REM' units. Ultimately, I needed to create a fallback for this scenario: in this case, a 'PX' unit fallback.

Performance and Coding Speed

You'll find a lot of helpful resources on the web for the topic 'REM to PX Mixins' (see related articles) which I've used as part of the research for this article. The majority of these rely on the same technique: using a Sass Mixin which generates the CSS property twice, for example :

SCSS:

.header  {
    @include rempx(margin, 1rem 2rem);
}

outputs to:

CSS:

.header  {
	margin:16px 32px; /* Fallback */
	margin:1rem 2rem;
}

The main problem with this is that every CSS property using this mixin is duplicated, and your overall CSS weight will grow. This technique serves two properties instead of one, and falls back to the first if 'REM' units aren't supported by the browser. It's not very clean, but it's smart and easy to maintain. Because we know 'REM' units aren't supported in IE8-, what if we served two different stylesheets?

The second problem with the above method is typing-speed. Typing @include rempx(property, values) each time you want to add a new CSS property isn't optimal.

Serving Different Stylesheets

Our first goal is to have two separates stylesheets:

  • A 'main.css' file for standards Browsers, using 'REM' units.
  • A 'main-ie.css' file for IE8-, using 'PX' units.

(both of these files will have exactly the same CSS contents, only the units are different)

Nicolas Gallagher shows us how to serve a basic CSS for standard browsers and a different CSS for IE8- using this code:

<!--[if (gt IE 8) | (IEMobile)]><!-->
      <link rel="stylesheet" href="/css/main.css">
<!--<![endif]-->

<!--[if (lt IE 9) & (!IEMobile)]>
      <link rel="stylesheet" href="/css/main-ie.css">
<![endif]-->

Standard browsers will download the 'main.css' only, and IE8- will download the 'main-ie.css' only. This ensures that the client doesn't have to download both css files.

Our second goal is to keep the "@mixin" technique so that we only have to maintain one file.

Sass to the Rescue!

Now that we know how to serve different stylesheets, we need to generate two CSS files. This is were Sass comes in. To keep the example simple I won't use a special file structure for Sass, just a bunch of .SCSS files. Here is the structure for our example:

  • An empty 'css' folder.
  • A 'sass' folder with four files:
    • main.scss
    • main-ie.scss
    • _functions.scss
    • _module-example.scss

MAIN.SCSS:

// config
$px-only:false;

// import sass functions
@import "functions";

// import site modules
@import "module-example.scss";

MAIN-IE.SCSS:

// config
$px-only:true;

// import sass functions
@import "functions";

// import site modules
@import "module-example.scss";

_FUNCTIONS.SCSS:

$pixelBase : 16; /* 1 */


@function parseInt($n) {
    @return $n / ($n * 0 + 1); /* 2 */
}


@function u($values){ /* 3 */


    $list: (); /* 4 */


    @each $value in $values { /* 5 */
        
        @if (type-of($value) == "number") and ($value != 0) { /* 6 */


            $unit : unit($value);     /* 7 */
            $val  : parseInt($value); /* 2 */


            @if ($px-only) and ($unit == 'rem') { /* 8 */
                $list: append($list, ($val * $pixelBase) + px); /* 8 */
            }


            @else if($unit == 'px') or ($unit == 'rem'){ /* 9 */
                $list: append($list, $value); /* 9 */
            }


            @else {
                @warn 'There is no unit conversion for "#{$unit}"'; /* 10 */
            }


        }@else {
            $list: append($list, $value); /* 11 */
        }


    }


    @return $list(); /* 12 */


}

How Does it Work ? Explanations.

The variable $px-only isn't scoped in the 'main.css' or 'main-ie.scss' only. All imported files are impacted by the value of $px-only. When u() function is called, it refers to the value of $px-only declared in the 'main.scss' or 'main-ie.scss' files. This Sass unit function must be imported before the other modules in order to work.

Function Explanations:

  1. This variable refers to the default browser's font-size, and 1rem = 16px. For more maintainability, we store the value in a variable in order to reuse it or to change it easily.
  2. This function returns a number from a given string, thanks Hugo for it! http://hugogiraudel.com/2013/03/18/ultimate-rem-mixin/
  3. We called our function u because it's faster and easy to type. 'u' also stands for 'unit'. This function takes one argument which is the CSS values with multiples REM or PX numbers, e.g u('5rem 2px').
  4. We define a new list, where we'll store new values.
  5. For each value passed in $values argument, test the current value and store it in the $list array.
  6. Test if the current value is a number and if that value isn't equal to 0.
  7. This Sass function returns the unit associated with a number. In our case, 'PX' or 'REM'.
  8. First condition: if 'pixels only' mode is active and unit value is 'REM', convert 'REM' value in pixels and push it into $list array.
  9. Second condition: else if the units are 'PX' or 'REM' we just push value into list.
  10. If the first and second conditions fails, we throw a warning in Sass console.
  11. If the first condition on 6) fails, just push the current value in the list.
  12. Then, we return the whole list as a clean CSS string values.

Usage

_MODULE-EXAMPLE.SCSS:

.main-header {
    margin:u(1rem auto 10px);
    padding-bottom:u(0.25rem);
    font-size:u(0.875rem);
}

Outputs to:

MAIN.CSS:

.main-header {
    margin: 1rem auto 10px;
    padding-bottom: 0.25rem;
    font-size: 0.875rem;
}

MAIN-IE.CSS:

.main-header {
    margin: 16px auto 10px;
    padding-bottom: 4px;
    font-size: 14px;
}

Pros & Cons

Pros

  • Easy to maintain: Just edit your code once, and two separate stylesheets will be generated.
  • Easy to read: Your final CSS is clear and without unused properties.
  • Lower file size: Your final CSS is lighter.
  • Faster to develop: Just type u() to generate the final units.

Cons

  • Needs a hack in the <head> to serve the right stylesheet.

I'd love to get your feedback on this technique, and share any ideas you might have on how it could be improved!

Related Articles

This code is also available on GitHub and Bower.

Sébastien Axinté

About Sébastien Axinté

Hi, I'm Sébastien. I live in London and work as a Front-end Developer at BBC. I like building scalable and maintainable applications in various environments; web, desktop and mobile. I'm also a huge Quake 3 fan. Let's chat about development on Twitter.

Recent Features

Incredible Demos

  • By
    MooTools Fun with Fx.Shake

    Adding movement to your website is a great way to attract attention to specific elements that you want users to notice. Of course you could use Flash or an animated GIF to achieve the movement effect but graphics can be difficult to maintain. Enter...

  • By
    iPad Detection Using JavaScript or PHP

    The hottest device out there right now seems to be the iPad. iPad this, iPad that, iPod your mom. I'm underwhelmed with the device but that doesn't mean I shouldn't try to account for such devices on the websites I create. In Apple's...

Discussion

  1. I know some sass trick to use REM with Sass but this one is certainly the simplest to use!

    This one is also interesting because it accepts either px or rem as an input value (great when you work from a photoshop file):
    http://hugogiraudel.com/2013/03/18/ultimate-rem-mixin/

    • Exactly, and the Hugo’s article helps me to write the article.

      The approach is slightly different, you can also mix both values ‘px’ or ‘rem’ but ‘px’ aren’t converted for the standards browsers stylesheet. (intentionally wanted)

  2. Tho Vo

    Nice article! Really helpful!
    I think it would be better if we just keep what properties need to convert from rem to px, like margin,padding and font-size in main-ie.css but the other like font-family or background-color for example can still keep in the main.css. It may help reduce the size of main-ie.css :)
    And Axinté, do you test it with other properties like background-size, background-position and multiple background…

    • Thanks Tho !

      You can do this yes, but the main goal will change and it’s not optimal. You’ll need three css:
      – one css with your common properties without units. (font-family, backgrounds..)
      – one css with properties in rem units for standards browsers.
      – one css with properties in px units for IE8- browsers.

      This force the browser to make one more HTTP request. And the final CSS weight remain the same, but splitted in 2 files.

      I didn’t tested all css properties but the logic is the same, for multiples background position you can do the following:

      background-position: u(1rem 2rem), u(3rem 4rem);
      
  3. Karl Merkli

    Looks like my Sass Library Frost–Fallback

  4. Tho Vo

    Hi Sébastien,

    My idea is we just load two files
    1. main.css with full of properties and rem unit
    2. main-ie.css for IE8 with px unit for only properties need it, I mean we still keep all properties like font-family,background-color for example in main.css(as we know we must load main.css for sure) and in main-ie.css(width conditional comment we load only when user browser is IE8) keep only properties need to change from rem to px, so the main-ie.css will be a bit smaller than main.css

    And I will try this for background-position in my next project,I hope I can notify you any problem :)

    • Yes I understood, but IE8- will download two CSS files :) And the whole stylesheets weight for IE8- will be main.css + main-ie.css.

      Like I said in the article, with the Nicolas Gallagher conditional comments, you can avoid this:
      – one css for ie. (full properties and px units)
      – one css for standards browsers. (full properties and rem units)

      It’s better than a fallback. Notify me of any problems you’ll find ! :)

  5. This is great!

    I’ve written a px and rem mixin which required you include the property as a parameter:

    https://gist.github.com/larrybotha/4153104

    I didn’t consider that using a function instead would clean up the CSS, though.

    This is an awesome utility, thanks Sébastien.

  6. Hello, Sébastien!

    Thanks for that. It is pure gold and shall improve a lot of people’s CSS and also accessibility as a whole.

    I’ve been using a similar technique for a while now, but was not generating two CSS, like your approach. Also, I was using @include rem(margin, 0, 20, 30, 40) as a syntax, which was not helping on my speed so this is a huge improvement. Again: thanks!

    One thing, tho: I thought you could still improve it a bit by supporting a call to the function like this:

    margin: u(0px auto 10px);
    

    So, basically, before the conversion for $unit and $val variables you could try testing for ‘auto’ as a value, as such:

    @if $value == 'auto' {
        $list: append($list, $value);
    }
    
    @else {
        /* Your code goes here */
    }
    

    Hope it helps :)

    • Thank you for your comment, I really appreciate. I had not thought about this improvement, that’s a good idea :)

  7. I’ve integrated this into my workflow now – as I already have to serve a LT IE9 stylesheet in this particular project, I see no cons!

    Thanks very much for this post Sébastien :)

    • Awesome, glad to see it’s working for you! :D

    • frayde

      Using JavaScript to “polyfill” is the last solution to use in my experience. We’re talking to use a css preprocessor to produce an ie8 dedicated css and a conditonnal comment to load it only in the concern browser.

      So there are a lot of possibilities to polyfill IE8 lack of rem unit support, but using css to polyfill css only in the right place and affecting only the guilty browser seems to be a wiser choice for your visitors.

      Bye.

  8. Ariel

    Hey! thanks for this! the only problem I encountered is that if you put 0 as a value on your property, it gets ignored.

    Like this:

    .class {
        margin: 2rem 5rem 0;
    }
    

    Gets converted to:

    .class {
        margin: 2rem 5rem;
    }
    

    or on pixels:

    .class {
        margin: 32px 80px;
    }
    

    Do you know a fix for this?

    Thanks!

    • Ariel

      Nevermind, I just added this line to the one added by Arthur Gouveia and it worked:

      @if $value == 0 or $value == 'auto' {
             $list: append($list, $value);
      }
      

      Now the only thing that doesn’t work is the border shorthand property for example. Anyone knows a workaround for that?

    • Ariel

      I guess I’ll be commenting myself over and over :P

      This fixes the shorthand problem. Ultimately you would use this:

      @if $value == 0 or type-of($value) != "number" {
             $list: append($list, $value);
      }
      

      This lets you use shorthand or non numerical characters and works perfectly!

    • Goran

      Hi guys, thank you so much. I’ve tryed your code and it works great.

      I add Ariel fix:

      @if $value == 0 or type-of($value) != "number" {
                      $list: append($list, $value);
                  }

      BUT when I try to set:

      margin:u(2rem auto);

      I get error:

      Syntax error: $number: "auto" is not a number for `unit'
      

      Thanks

    • Goran

      Sorry for posting again, but I also tryed to add:

      $value == 'auto'

      no help.

      So, to make clear, with Ariel fix:

    • Hi Alex,

      Thanks for your comments and your self-reply, I appreciate that :)

      At first glance, I guess you passed the property directly within the u() function and it wasn’t designed for it (only numbers by default).

      But your improvements are smart and useful, thanks for it !

    • Matt

      Hey Ariel & sebastien

      I am trying to add the following into the mixin.

      @if $value == 0 or type-of($value) != "number" {
            	$list: append($list, $value);
      			}
      

      Where do I put it? I keep getting errors when it compiles it?

      cheers

    • Tom

      Sebastien – thanks a million this is the neatest solution to this problem I’ve come across by a mile. Can even use autoprefixer to only target ie fixes to the ie css now!

      Ariel – thanks for your addition, Sebastien – you should add this into your main article, I came here to add the same suggestion myself.

      Only thing I have to add is that “type-of” should be “type_of”.

      Matt – you just need to add this to functions.scss as the first condition (before the first “@if”, and change that to a “@else if”)

  9. As there is Opera Mini who doesn’t understand rem, too (http://caniuse.com/#search=rem), I tend to use rem besides px inside one CSS.

    And do you really use rem with padding and margin? I concentrate it on font-size. But I will test margin and padding in my next project.

  10. Hi Jens,

    It depends on the specific case, I could use “em” instead of “rem” sometimes, maybe when I need margins or paddings linked to my element font-size. If not, I will use “rem” instead.

    For browsers that doesn’t support rem, you can use a Polyfill. (I didn’t tested it yet.)

  11. CharlesBiggs

    This is real nice. Just a great way to remove pixel clutter and verbosity. Added it to my toolbox and integrated in my workflow.

    Big up :-]

  12. Thanks, this is excellent! I integrated it into my latest project; works great.

    • In a few cases I wanted the function to convert px to rem, so I used the following:

      @else if($unit == 'px'){
          $list: append($list, ($val / $pixelBase) + rem );
      }
      
  13. Paul McElligott

    If I use the conditionals like you have it there, Chrome doesn’t load the default CSS. I’m using word press and this trick to wrap the style sheet commands in the conditional statements.

    http://code.garyjones.co.uk/ie-conditional-style-sheets-wordpress

    If I just use the second one –

    [if (lt IE 9) & (!IEMobile)]

    , it seems to work.

  14. Sass arithmetic is dimensional, so you don’t need to do all these backflips to strip off units and parse strings. You just need to do the math correctly. Basically the units cancel like x’s and y’s did in algebra.

    So:

    $px-value: $rem-value * 16px / 1rem; // 2rem => 32px
    

    Here’s how I coded up the rem-fallback concept the other day. It uses some more advanced Sass features available in Sass 3.3: https://gist.github.com/chriseppstein/1241eee7fa6ccae7ec0c

  15. This seems to work (to allow for auto or 0 within the input)

    @function u($values){ 
    
          $list: (); 
    
          @each $value in $values { 
    			
    	@if $value != 0 and type_of($value) == "number" {
    		$unit : unit($value);
    		$val  : parseInt($value); 
    		
                    @if ($px-only) and ($unit == 'rem') { 
                          $list: append($list, ($val * $pixelBase) + px); 
    		      }
    		@else {
    		      $list: append($list, $value);
    		      }
    	}
    	@else {
    	      $list: append($list, $value);
    	      }
          }
          @return $list(); 
    }
    
    • Yes, this fix works and I tried it at work on a production environnement. This is the same fix that Ariel and Arthur Gouveia found, but your code should work as well :)

  16. Matt Oettinger

    Love this solution but am wondering – how would I change the $pixelBase based on media queries?

  17. Matt Oettinger

    Answering my own question. I don’t think I’d need to change the $pixelBase but instead would change the font-size tied to the base html element depending on media queries.

  18. Yep, the $pixelBase variable shouldn’t be changed because we multiply this number with the REM value in order to get the right pixels value, and 1rem = 16px by default in browsers.

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!