CSS Macros

"CSS does not need variables. If you need variables, you're doing it wrong."

Fork me on GitHub

This was certainly true when CSS was a "set once, style forever" technology.

However, that's no longer how we build webpages. Modern websites use dynamic styling, by manipulating css declarations for elements and classes, using selectors-based manipulation (jQuery, MooTools, document.querySelector, etc). Even though a designer can write CSS in such a way that it's obvious which elements share which declarations, for instance by using "p, li, a { color: #333; }", shared declaration are lost after the CSS is loaded. The CSS engine throws that implicit information away, and binds rules separately for each element indicated.

You might not think that's reason enough to want CSS variables, using the counter argument to "use classes". While for a low number of shared declarations, for a low number of elements, this might seem a good idea, for modern websites, with dynamic styling, that becomes intractible. Let's look at a realistic scenario where a set of {p, span, pre, ul, ol, li, h1, h2, h3, h4} might have shared styling for fonts for {p, span, li}, shared text color styling for {p, pre, li, h1, h2, h3, h4}, shared border styling for {pre, ul, ol} and different shared border styling for {p, span, h1}, shared offsets for {pre, ul, ol, h2, h3, h4} and shared backgrounds for {pre, ol, ul}. Modifying the shared style now requires either:

  1. modeling the "shared" property via classes, requiring you to add four or more classes to the HTML markup for pretty much every element (this will make the page hugely bloated, impossible to maintain, and invites typos like it's nobody's business), or
  2. modelling the fact that certain elements share styling in JavaScript, so that you can call one function to change the styling on all elements that need it applied, hard-coding which element should be styled how (this means you now have two places that have to agree on which elements share what, and have to be maintained in parallel).

So neither of these options are very attractive.

Okay... so how are you solving it?

CSS macros. You can solve the above problem relatively simply with some CSS variables, so that's why the JavaScript solution presented on this page exists: it makes implementing a design a lot easier. Now, it's not perfect: it requires JavaScript, which means that if you want to do it right, you have to write fallback rules (such as a "style-no-js.css" file, or double declarations for elements) for browsers that don't do JS —not just old browsers, but also current browsers with NoScript or the like enabled— and then write your dynamic, manipulable CSS with macros as second stylesheet. Of course you should already be writing fallback rules if you're using dynamic CSS, so it's not really going to change a ridiculous amount on that front. It just makes applying and changing design styles on-the-fly a hell of a lot easier.

Download the following JavaScript include and it will add macro capabilities to the CSS engine (technically, to JavaScript) when included, so that you can do this:

@macros {
  text-font: Georgia;
  text-font-color: #06A;
  standard-border: 1px solid #66F;
  subtle-border: 1px solid #999;
  quote-margins: 0em 2em;
  block-bg: #dd9;
}

p {
  font-family: text-font;
  color: text-font-color;
}

span {
  font-family: text-font;
  color: text-font-color;
}

pre {
  padding: 5px;
  color: text-font-color;
  border: standard-border;
  margin: quote-margins;
  background-color: block-bg;
}

ul{
  border: standard-border;
  margin: quote-margins;
  background-color: block-bg;
}

ol{
  border: standard-border;
  margin: quote-margins;
  background-color: block-bg;
}

li{
  font-family: text-font;
  color: text-font-color;
  margin-right: 1em;
}

h1,h2 {
  border-bottom: subtle-border;
}

h1,h2,h3,h4{
  color: text-font-color;
  padding-left: 0.2em;
}

h3,h4 {
  margin: quote-margins;
}

Without everything breaking.

What about LESS or SASS?

I don't like LESS because I find the syntax horrible. I am someone who genuinely doesn't understand why he needs to use special syntax to mark a variable as such when there's only one place where it can be used. It's one of the things I like about Java and JavaScript: if its role does not conflict with anything, don't add syntactic sugar to emphasis that role. I also don't like how its syntax doesn't keep with CSS conventions. It makes a LESS flavoured CSS file look like a coding kludge. It's just not for me.

I do like SASS, but it doesn't solve the problem of the CSS engine taking apart your grouped declarations and wiping out the shared design elements. Changing the design on the fly is still as impossible with SASS as it is without it. As SASS is a serverside "you write a much nicer kind of stylesheet, and the SASS compiler turns it into CSS for use on your webpage" technology. This means that at the client, SASS generates exactly the same kind of CSS as you currently use. The generation process is just much nicer.

However! If you use SASS, you can safely combine it with cssmacros.js and still have the benefit of client-side css macros ('constants' or 'set-once variables') while being able to write your styling in a far more sensible stylesheet language. If your setups support ruby, use both!

How to use macros in your CSS

Add the JavaScript include to your page's <head> element:

<script type="text/javascript" src="cssmacros.js"></script>

Then, simply add the following rule to the top of your CSS:

@macros {
  macro_1: value;
  macro_2: value;
  ...
}

and you can use those macros in the rest of your stylesheet:

p {
  color: macro_1;
  background-color: macro_2;
  ...
}

But remember that just like for any other dynamic CSS, you need to declare fallback values, or you risk looking horrible before people enable scripts (or upgrade to a better browser :)

Global macros

You can also declare global macros for use in any stylesheet in which case you'll want to use @global-macros {} instead:

@global-macros {
  macro_1: value;
  macro_2: value;
  ...
}

and you can use those macros in any stylesheet. Because the macros aren't replaced until DOMReady is signalled, you can even effect macro replacement in stylesheets that are loaded before the stylesheet that has a global macros block.

Additionally, you are not limited to a single global macros block. You can declare as many as you like, as long as you observe these two properties:

  1. Local macros take presedence over global macros.
  2. Macro redeclaration is (load) order based.

How to change macros

Macros declared using @macros {} are stylesheet-specific, and the base functionality is tied to individual objects in the document.styleSheet array (CSSStyleSheet objects, to be precise). There is also a general purpose "do this for all stylesheets" API, so you get access to the following base functions:

  1. document.styleSheets[n].getMacro("macro name");
  2. document.styleSheets.getMacro("stylesheet name", "macro name");
  1. document.styleSheets[n].setMacro("macro name", "new value");
  2. document.styleSheets.setMacro("stylesheet name", "macro name", "new value");
  3. document.styleSheets.setMacroForAll("macro name", "new value");

The general purpose styleSheets.(g/s)etMacro require the stylesheet's href value, so if you're using "style.css" then you use that are first argument.

cssmacros.js and jQuery

If you're using jQuery, you also get access to a .macro() function that is similar in functionality to the jQuery .css() function:

  1. $('link').macro("macro name");
  2. $('link').macro("macro name", "value");

The first returns a single value, the second a standard jQuery set after applying the modification. To modify a specific style sheet, you simply specify this in your selector:

var focalColor1 = $('link[href="style.css"]').macro("focal-color-1");

CSS macros in action

Obvious this page uses css macros, so let's apply some changes:

These changes are all effected by single calls, only modifying the macro value for the indicated macro. No selectors or classes are used, so you can't "forget" elements in a selector. If the CSS says that macro is used, then changing its value applies to everything that uses that macro. It's that simple.