Generating Alternative Stylesheets for Browsers Without @media

By  on  

If your CSS code is built with a mobile-first approach, it probably contains all the rules that make up the "desktop" view inside @media statements. That's great, but browsers that don't support media queries (IE 8 and below) will simply ignore them, ending up getting the mobile view — not good.

An alternative is to create two separate stylesheets — a regular one to be served to modern browsers, and another without media queries, served to older browsers that don't support them. But is this maintainable? Is it reasonable to keep a separate stylesheet for a small subset of visitors?

Turns out that, as Jake Archibald described, Sass can be used to automate a great part of the process. In fact, breakpoint managers such as include-media or Sass MQ provide a mechanism for dealing with this quite easily and with no extra maintainability effort.

In this article, I'll describe how to use include-media to generate an alternative stylesheet without media queries, as well as to how to integrate that process in the workflow of popular build tools.

A first approach

So how do we go about generating this alternative stylesheet? We want to get rid of all @media statements, but it's not as simple as unwrap each one of them all and use its contents.

For example, let's imagine a grid of articles. On a mobile view, each article should take the full width of the screen, but as the viewport width grows, more articles should be fitted into one row. To do that, we gradually decrease the width of the article relative to its container with a few media queries — by the time we get to 1400px, we'll be able to fit 8 articles per row.

/* Original rules (1) */
.article {
    width: 100%;

    @media (min-width: 768px) {
        width: 50%;
    }

    @media (min-width: 1024px) {
        width: 25%;
    }

    @media (min-width: 1400px) {
        width: 12.5%;
    }
}

/* All media queries flattened (2) */
.article {
    width: 100%;
    width: 50%;
    width: 25%;
    width: 12.5%;
}

Without any special treatment, IE 8 would simply ignore all media queries and therefore render full width articles, even on a large screen (1). However, if we blindly flatten all media queries and use their contents, the browser will end up using the last rule at all times (width: 12.5%, in this case), even thought it was originally intended for exceptionally large screens only (2).

That's not ideal, as we've gone from ignoring all media queries to the other extreme of including them all — which on a typical site probably means going from a mobile view to a really large one. A more reasonable approach is to pick a specific width and select only the media queries that contribute to that view.

Enter include-media

Moving into include-media land, let's start with the same list of breakpoints.

$breakpoints: (
    'small': 320px,
    'medium': 768px,
    'large': 1024px,
    'super-large': 1400px
);

It's fair to assume that our IE 8 users won't be on a phone or a tablet, so it seems reasonable to serve them the website as it looks on the large view, so 1024px. Let's take a look at a few media queries from one of the modules of our website, part of _module1.scss.

.module1 {
    // This rule interests us, as it affects 'large'
    @include media('>=phone') {
        color: tomato;
    }

    // This one doesn't, as it's between 'medium' and 'large' (excluding)
    @include media('>medium', '<large') {
        color: chocolate;
    }

    // Not this one either, it affects only 'super-large'
    @include media('>=super-large') {
        color: wheat;
    }
}

With include-media, you can simply tell the library that you want to generate a version of the stylesheets without media queries support, and specify which breakpoint you want to emulate. To do that, you simply set two variables ($im-media-support and $im-no-media-breakpoint respectively).

A typical scenario is to have two copies of the main SCSS file: the normal one stays as is (main.scss), and the alternative one with the variables set (main-old.scss).

// main.scss

@import 'include-media';
@import 'module1';
// main-old.scss

$im-media-support: false;
$im-no-media-breakpoint: 'large';

@import 'include-media';
@import 'module1';

// Resulting CSS
.module1 {
    color: tomato;
}

You can even specify which media expressions to accept when flattening media queries. For example, if you have a media query that targets retina devices, you probably don't want to include its contents in the alternative style sheet, even if it matches the emulated breakpoint.

$im-no-media-expressions: (
    'screen',
    'print'
);

// This is retina only, we don't want this!
@include media('>=medium', 'retina2x') {
    color: olive;
}

Integrating build tools

Integrating this workflow into your build tools is quite simple and makes the whole process seamless. I'm including example tasks for both Grunt and Gulp (sorry Broccoli users, I never eat my greens).

// Gruntfile.js

module.exports = function(grunt) {
  grunt.initConfig({
    sass: {
      dist: {
        files: {
          'css/main.css': 'sass/main.scss',
          'css/main-old.css': 'sass/main-old.scss'
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-sass');

  grunt.registerTask('default', ['sass']);
};
// Gulpfile.js

var gulp = require('gulp');
var sass = require('gulp-sass');

gulp.task('sass', function() {
  return gulp.src(['sass/main.scss', 'sass/main-old.scss'])
    .pipe(sass())
    .pipe(gulp.dest('./css'));
});

gulp.task('default', ['sass']);

With that in place, the build tool will generate both style sheets automatically, outputting them to css/main.css and css/main-old.css.

Serving the style sheets

Finally, we just need to serve the appropriate style sheet depending on the browser, using good old-fashioned conditional comments.

<!--[if lte IE 8]>
    <link rel="stylesheet" href="css/main-old.css">
<![endif]-->
<!--[if gt IE 8]><!-->
    <link rel="stylesheet" href="css/main.css">
<!--<![endif]-->

And that's it. IE-friendly mobile-first responsive websites!

You can get include-media at http://include-media.com

Eduardo Bouças

About Eduardo Bouças

Eduardo is a Portuguese web developer based in London, working as Lead Developer for Time Inc. UK. He's passionate about the web, clean design, elegant code and robust solutions.

Recent Features

  • By
    Write Better JavaScript with Promises

    You've probably heard the talk around the water cooler about how promises are the future. All of the cool kids are using them, but you don't see what makes them so special. Can't you just use a callback? What's the big deal? In this article, we'll...

  • By
    CSS Filters

    CSS filter support recently landed within WebKit nightlies. CSS filters provide a method for modifying the rendering of a basic DOM element, image, or video. CSS filters allow for blurring, warping, and modifying the color intensity of elements. Let's have...

Incredible Demos

Discussion

  1. Interesting approach, but why not use something like respond.js which adds basic mediaquery support?

    • That’s a perfectly valid approach as well.

      This article came about after we’ve implemented this fallback method as part of include-media and I guess it’s a less ambitious approach than respond.js. Instead of trying to make media queries work on older browsers, we just accept the fact that they don’t and try to serve a “static snapshot” of the website at a certain breakpoint instead.

    • I see, was just wondering if there was some particular reason of doing it like this instead of just throwing respond.js at it. I guess in a sense this is a bit simpler and less error-prone than using a script.

  2. According to https://adactio.com/journal/4494/ a conditional comment

  3. You may want to exclude Windows Phone 7 in the conditional comment: [if (lte IE 8)&(!IEMobile)] (https://adactio.com/journal/4494/)

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