CSSOM, Vars, Mixins, Nesting, and Modules

Last updated:

I recently gave a talk about some experimental CSS improvements that Chrome engineers are working on. My slide deck was shared pretty widely, but it's not really designed to be looked at without me standing in front of it providing more background. So, here's a blog post talking about the same stuff. It's almost like I'm standing right beside you, whispering and secretly judging you. Always judging.

Many of these improvements are directly inspired by CSS preprocessors like SASS and LESS.

NOTE: Everything described in this post is experimental, and in the process of being proposed to the CSS Working Group. All syntax may vary, possibly significantly, when the experimental implementation is finally exposed, and more when this is properly standardized. Just to drive it home, I've tweaked most of the syntax in this post from what was written in my slide deck, because we talked about all this stuff yesterday.

CSSOM Improvements

The CSS Object Model (or CSSOM for short) is the javascript interface to CSS. You use it anytime you write code like el.style.width = "500px";. It's been with us for a while, and it sucks.

Now, don't get me wrong. The CSSOM does its job. You want to read and write CSS values from your script, it can do the job reliably. But it's based entirely around strings. When you ask what the width of an element is, you don't get a number, you get something like "10em" or "200px" or "50%". You have to split out the number and the unit, and if the unit is anything other than "px", figure out how to translate the number you got into a more usable value. Don't even get me started on the headaches when some of the cool new ways to specify lengths appear - parsing calc() expressions will be a big bag of suck.

We can do better than this. Why should you have to do string-parsing when the browser already knows what the value and unit is? Why should you have to try and carefully disentangle exactly how to convert between 'em' and 'px' (check the parent's font-size, no wait, it doesn't have one, check the next parent, crap that one's font-size is also in ems, so check the next parent up...) when the browser knows how to do that already? Why should you have to carefully parse a color out of the 7 different formats it can come in just to adjust the red value of it? Don't even get me started on trying to figure out how to adjust the size of the ellipse in a radial gradient.

The new improvements to the CSSOM we're implementing solve all these problems and more. Your code will look something like this:

// Increase the width of an element by 100px
el.style.values.width.px += 100;

// Darken the background-color
el.style.values.backgroundColor.lightness = 10;

// Get the width of an element's border in ems
var emwidth = el.style.values.borderWidth.em;

// Extract a list of all the text-shadow colors
var shadowColors = 
  el.style.values.textShadow.l.map(
    function(v){return v.color;}
  );

// Create a value with the right CSSOM interfaces from JS
var myColor = new css.Color(0,0,255);
myColor.alpha = .5;
el.style.values.color = myColor;

Pretty much all heavy JS development involves tweaking CSS a lot. These changes should make this sort of thing tons easier and less buggy.

There's further discussion happening in this area about what other functionality we could usefully expose to page authors. For example, while it's great to be able to measure the width of an element in any unit, sometimes you just want to measure an arbitrary length that you'll use elsewhere. You might want to find out how wide 50% of an element would be, so you can create a dialog box with that width. Rather than trying to do tricky things with creating elements, setting their widths, and then measuring them, we can just offer an ability to directly convert lengths with respect to some element.

CSS Variables

People have been requesting the ability to create variables in CSS for literally a decade. The CSSWG has gone through periodic spasms of activity in this area, but it always got bogged down by arguments over the syntax and fine details. We're just gonna forge ahead with an experimental implementation of what we think is a good idea, and hope it's good enough that everyone can agree on it or something close to it.

Variables let you define a value once and then use it in multiple places. This is great for tons of things. For example, you can define your sites color-scheme in variables and then, when you change the theme, just change the handful of variables rather than every single place where a color is referenced.

Both declaring and using variables are really easy:

@var $important-color #c06;

h1 { color: $important-color; }
strong { color: $important-color; }
.dialog { border-color: $important-color; }

You can read and write these variables from javascript, too:

css.vars['important-color'] = "blue";

Optionally, you can declare your variable with a type, so the new CSSOM improvements work on it:

<style>
  @var color $important-color #c06;
</style>
<script>
  css.vars['important-color'].red = 255;
</script>

(If you omit the type, you just get the old CSSOM, so it's just treated as a string.)

The $ character is a required part of this syntax. It's how the CSS parser knows that you're talking about a variable, and not just some existing keyword.

CSS Mixins

Variables are great at reducing repetition and making your stylesheets easier to edit, but they can only hold a single value. What if you want to reuse a set of properties in multiple declaration blocks? HERE COMES THE MIXIN:

@mixin error {
  background: #fdd;
  color: red;
  font-weight: bold;
}
div.error {
  border: thick solid red;
  padding: .5em;
  @mix error;
}
span.error {
  text-decoration: underline;
  @mix error;
}

You can add multiple mixins to the same block, override values imported from a mixin (just set the property again after the @mix directive), and even nest mixins into other mixins.

Even better, mixins can be parametrized, so they act like functions:

@mixin rounded-corners($size 8px) {
  -moz-border-radius: $size;
  -webkit-border-radius: $size;
  border-radius: $size;
}
.foo {
  @mix rounded-corners(1em);
}

Look, ma, all the benefits of vendor prefixes, none of the pain!

CSS Nesting

In any large stylesheet, you end up repeating a bunch of selectors. You may need to style some element, and then style it when hovered, then style img descendants of it, then style a ::before on it, etc. This repetition makes the stylesheet harder to read, harder to write, and harder to edit without introducing subtle bugs. To help with these problems, we'll be introducing the ability to nest selectors inside of declaration blocks:

#main > header > h1 {
  font-size: 2em;
  background: blue;
  color: white;
  & > a {
    color: #ddf;
    &:visited {
      color: #fdf;
    }
  }
}

...which is syntax sugar for...

#main > header > h1 {
  font-size: 2em;
  background: blue;
  color: white;
}
#main > header > h1 > a {
  color: #ddf;
}
#main > header > h1 > a:visited {
  color: #fdf;
}

The & character indicates "here there be nesting", and acts like a reference to the outer selector. You can use any combinator (descendant, child, sibling...), and modify the original selector by adding more classes, pseudoclasses, pseudoelements, and so on. Specificity is unaffected, since this is just syntax sugar - to figure out the specificity, just unroll the nesting and figure it out normally.

You can even combine Mixins and Nesting together, so that adding a mixin also styles hover states, pseudoelements, descendants, etc.

We'd like to allow the selector reference to be placeable somewhere other than front of the nested selector, but we haven't yet figured out an elegant way to allow that and still have an explicit indicator that nesting is happening. (If you don't have an explicit indicator, it requires a lot of lookahead sometimes to distinguish between nesting and a property. We try to avoid adding large lookahead in the CSS parser.)

CSS Modules

Variables and Mixins are great, and really useful for creating CSS frameworks and distributing widgets, but they have a significant problem: their names are global. If a widget distributes some CSS with a few vars to let you easily customize the style, it has to make sure that the variable names won't clash with variable the page author might have defined. This results in prefixes embedded in every name, which is annoying and verbose. Instead, we'll be introducing a very simple and lightweight "module" system, inspired by Python's module system, that lets authors group their vars and mixins under a common name.

@module myWidget {
  @var $foo red;
  @mixin bar { prop: value; }
}

Unless there's a $foo variable declared in the global scope, the following is an error:

foo {
  color: $foo;
}

But either of these will work:

foo {
  color: $myWidget.foo;
  @mix myWidget.bar;
}
bar {
  @use myWidget;
  color: $foo;
  @mix bar;
}

You can nest modules in another module, if you like hierarchical organization. Other directives that create global names should also be nestable into modules, like @font-face and @counter-style.

CANIHAZNAO??!?

Not quite yet, no. Right now, these features only exist on the computers of a few developers here at Google. I have good news, bad news, and then more good news.

Good News: I can't give any promises, but the time until this starts showing up in nightly builds of Webkit is measurable in months. Variables and unparametrized mixins should be first. Once they hit public, these things should be usable in "protected spaces" like Chrome extensions or apps from the Chrome Web Store.

Bad News: This stuff really doesn't have good backwards compatibility. If you go to the necessary effort to make it so, it defeats the point of using it in the first place. So, this is one of those things that you'll have to wait a decade for before you can reliably use in the public web.

More Good News: All of this is just syntax sugar - it all bakes down into normal CSS. CSS preprocessors like SASS and LESS already exist that turn specialized/improved variant syntaxes into vanilla CSS. The same can exist for these syntax improvements; in fact, it's pretty likely that SASS will have a mode for this syntax built directly in (they already have an SCSS mode which uses a syntax much closer to real CSS). Once a preprocessor is built, you can start using the syntax immediately for your public site, potentially even before this becomes public in Webkit!

(a limited set of Markdown is supported)