Descartes

Writing CSS in JavaScript

Hello, world! I'm Jon.



Stack Overflow
Marketing Engineering Lead


I run all engineering support for the Stack Overflow brand. Infrastructure, campaign production, and evangelism. I think a lot about growth and what people think about us.

Bento
Founder


My passion project building tools to help self-taught developers find the best places to learn to code. I think a lot about how people learn, especially beginners.

What's on the menu?



1

Motivation

What is Descartes? Learn more about the inspiration for this experiment and why I built this library.

2

Features

A quick tour of all the things that Descartes can currently do, showing off some of its features.


3

How It Works

This is how I actually built the project, and what the approaches were to solving some of techincal issues.

4

What's Next

This is still an experiment, and there's a lot to still try out. Find out what I'm still thinking about.

1

Motivation

You've got to be joking right?

Of course! But maybe...


Writing CSS with JavaScript is crazy, of course!

But maybe...

Are you a wizard?


Of course, you should use CSS to do your styling whenever possible. If I want to simply vertically align something like a div, this makes absolutely perfect sense.

.verticalAlign {
  position: relative;
  top: 50%;
  transform: translateY(-50%);
}

...right? Of course! Of course this makes sense! I do this all the time.

But maybe...that's just stupidly unreadable


Maybe writing CSS makes you feel like you're invoking magical spells and incantations. Maybe if I really wanted to make a div element vertically align, I should be able to do so with some basic math and programming. Something sensible might use references to the DOM like this.

.verticalAlign {
  margin-top: (parent.height - this.height) / 2 + "px";
}

But of course not! DOM access and JavaScript in CSS is crazy! I would never do that in a production environment. Of course...

Cascading S**t Show


Of course, the cascade in CSS is workable. You've probably wondered why cascading works the way that it does. Why is everything global? Why does specificity denote priority? But it's workable! If you had to write plain CSS lately, it might get a little messy really quickly with all the namespacing and repitition.

html {
  margin: 0;
  padding: 0;
}
html body {
  margin: 0;
  padding: 0;
}
html body section {
  max-width: 800px;
}

On top of that, we have things like LESS and SASS, you say. Nesting rules together lets you get rid of worrying about specificity too much and even gives you things like variables and mixins. You can turn the CSS we had earlier and use a preprocessor instead. Of course!

html {
  margin: 0;
  padding: 0;
  body {
    margin: 0;
    padding: 0;
    section {
      max-width: 800px;
    }
  }
}

All you need to do is learn a new language and install the preprocessor and do just enough programming to get you by with styling. Easy, right? Of course!

But maybe...we already have JavaScript?

Maybe we could represent what you want in LESS or SASS in a JavaScript object literal and apply the styles yourself. Maybe you would also get all the nice built-in functionality of JavaScript with variables, loops, conditionals, functions, and you wouldn't need to use another technology pattern completely, maybe?

// LESS or SASS

html {
  margin: 0;
  padding: 0;
  body {
    margin: 0;
    padding: 0;
    section {
      max-width: 800px;
    }
  }
}
var styles = {
  "html": {
    "margin": 0,
    "padding": 0,
    "body": {
      "margin": 0,
      "padding": 0,
      "section": {
        "max-width": "800px"
      }
    }
  }
}

But of course not! Separation of responsibilities! I wouldn't dream of doing that in production! Of course...

But maybe...I have an idea



Thought Experiment

Again these aren't things I actually believe. I would never do these things in production, but it's certainly worth exploring as an exercise.

Existing Implementations

This is not the first time people have thought JavaScript should be used for CSS. Most notably, React has similar ideas and AbsurdJS is a precursor to the ideas here.

Recent Conversations

There are a lot of people talking about what it might mean to not use CSS anymore. One of my favorite talks is by @fat who created Bootstrap.

 

Descartes

Imagine styling if you had full access to the DOM, full control over the cascade, and all the programming power of JavaScript, without the need for LESS or SASS.



index.html
<!doctype html>
<html>
  <head>
    <script type="text/javascript" src="descartes.js"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
  <script type="text/javascript" src="styles.js"></script>
  </body>
</html>

styles.js
var rand_angle = function() {
  return Math.round(Math.random() * (180) - 90);
};
var rand_rgba = function() {
  return "rgba("+[255,255,255].map(function(x) {
    return Math.round(Math.random() * x);
  }).join()+", 1)"
};

Descartes.add({ // Just put descartes.js in the <head> tag
  "html": {
    "body": { // Nest selectors just like in Sass and Less
      "font-family": "Arial", // Set properties
      "font-size": 10 + 6, // = 16, converts to px
      "$(body).keyup": { // Bind events
        "background": function() { // To certain properties
          return 'linear-gradient('
            + rand_angle().toString() + 'deg,'
            + rand_rgba() + ','
            + rand_rgba() + ')'
        } // ...and use functions to generate property values!
      }
    }
  }
})
 

2

Features

What Descartes can do

Basic Application


At its core, Descartes lets you define styles as a JavaScript object literal to be applied to the DOM. There are several ways you can do this, but for small projects, you can apply things pretty directly.

Descartes.add({ // Completely inline
  "html": {
    "margin": 0, // Sensible px conversion
    "padding": 0,
    "font-family": "Arial", // Dashed CSS properties
    "body": { // Nesting
    	"margin": 0,
    	"padding": 0,
    	"background": "#fff"
    }
  }
})

Construction


However, defining a huge object literal doesn't make sense when you have a lot of styles. Breaking your styles apart and constructing them before passing it into the engine makes a lot more sense.

var styles = {} // Base style

var html = { // html rules
  "margin": 0,
  "padding": 0,
  "font-family": "Arial"
}

var body = { // body rules
  "margin": 0,
  "padding": 0,
  "background": "#fff"
}

// Do construction
html.body = body
styles.html = html

// Pass into the engine
Descartes.add(styles)

Flexibility


Breaking your styles apart also gives you a lot more flexibility to do things like share styles and modify them for special cases. Take these slightly different styles for p that has its font-size property changed when nested inside of a section

var styles = {
  "body": {
  	"margin": 0,
  	"padding": 0,
    "p": {
      "margin-bottom": 15,
      "font-size": 18
    },
    "section": {
      "background": "#fff",
      "p": { // Reused with small changes
        "margin-bottom": 15,
        "font-size": 16 // Modified
      }
    }
  }
}
var styles = {}

var body = {
  "margin": 0,
  "padding": 0	
}

var section = {
  "background": "#fff"
}

var p = {
  "margin-bottom": 15,
  "font-size": 18
}

styles.body = body
styles.body.section = section
styles.body.p = p
p["font-size"] = 16
styles.body.section = p // Modified

Values


Things get really interesting when you realize you have a lot more control over what values you can set using JavaScript. The first obvious thing you can do is do mathematical manipulations on number values.

var styles = {
  ".golden": { // Golden ratio rectangle
    "height": 400,
    "width": 400 * ((1 + Math.sqrt(5)) / 2) // 647.2135955
  },
  "p": {
    "font-size": 16, // sensible px conversion
    "line-height": 1.5 // no px conversion
  }
}

// Pass into the engine
Descartes.add(styles)

Functions


You can also use JavaScript to describe an entire ruleset for a selector. Take a closer look at the .golden class we had earlier that created a rectangle with golden ratio proportions. What if we wanted to create multiple rectangles of different sizes?

function golden(small) {
  return small * ((1 + Math.sqrt(5)) / 2)
}

var styles = {
  ".golden": { // Golden ratio rectangle
    "height": 400,
    "width": golden(400) // 647.2135955
  },
  ".golden2": {
    "height": 600,
    "width": golden(600) // 970.82039325
  }
}

// Pass into the engine
Descartes.add(styles)
function goldenRules(small) {
  return {
    "height": small,
    "width": small*((1 + Math.sqrt(5)) / 2)
  }
}

// Or just generate the rules directly
var styles = {
  ".golden": goldenRules(400),
  ".golden2": goldenRules(600)
}

// Pass into the engine
Descartes.add(styles)

DOM Access


JavaScript also gives you DOM access so you can use the properties of elements to do some really interesting things. Here's a naive reimplementation of mobile responsiveness using window.innerWidth.

var styles = {
  ".wrapper": { // Responsive wrapper
    "margin": "0 auto",
    "width": (window.innerWidth > 800) ? 800 : "100%"
  }
}

// Pass into the engine
Descartes.add(styles)

Now this will actually work for initial load, but it won't actually work when the window resizes. What's happening here is that (window.innerWidth > 800) ? 800 : "100%" only evaluates when Descartes.add() is fired, generally, when the document is ready.

Function Values


To give you more fine grain control, you can pass functions as values to any property you define in your styles. You simply return the value you ultimately want to use. The function gets fired on Descartes.add() but you can now re-evaluate the function multiple times during JavaScript execution. This would be the first step to giving you better control.

var styles = {
  ".wrapper": { // Responsive wrapper
    "margin": "0 auto",
    "width": function() {
      return (window.innerWidth > 800) ? 800 : "100%"
    }
  }
}

// Pass into the engine
Descartes.add(styles)

Aliases


Once you have functional values, how would you actually access those functions to fire again? You can basically get access to those functions using an alias, which is accessible using Descartes.alias. You just set the alias property of a selector, and you can access the function as a property of that alias:

var styles = {
  ".wrapper": {
    "alias": "wrapper", // Descartes.alias.wrapper is created
    "margin": "0 auto",
    "width": function() { // Descartes.alias.wrapper.width()
      return (window.innerWidth > 800) ? 800 : "100%"
    }
  }
}

// Pass into the engine
Descartes.add(styles)

// Now you can bind events to styles!
$(window).resize(function() {
  Descartes.alias.wrapper.width()
})

Using Elements


As an aside, Descartes will also pass in the individual element that is being selected as the first argument to the function you define. So, you can define styles that depend on an element by element basis.

var styles = {
  ".verticalAlign": { // Responsive wrapper
    "margin-top": function(elem) { // elem is the individual element
      return (elem.parentElement.height - elem.height) / 2
    }
  }
}

// Pass into the engine
Descartes.add(styles)

3

How It Works

Going under the hood

Background



1

ES2015

This was also an experiment in using ES2015 (or ES6? Eh?). There's a lot of really nice features that made writing Descartes easier.

2

Sizzle

The one external dependency I have baked into the library, this is jQuery's selector engine. Open source, and really really tiny.


3

Unstable

Stuff is changing here on an almost daily basis. What I'm showing here is the most bleeding edge version of Descartes, and the current docs may not be up-to-date.

4

Open Source

I'm looking for any help here if you're interested or have ideas about how to improve the library. There's a lot of stuff I want to explore, and with friends!

Sample


Take a look here at the simplest implementation here. I'll walk through each of the steps to show you construction to full style application.

<!doctype html>
<html>
  <head>
    <script type="text/javascript" src="descartes.js"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
  <script type="text/javascript">
    Descartes.add({
      "html": {
        "margin": 0,
        "padding": 0,
        "body": {
          "margin": 0,
          "padding": 0,
          "background": "#fff"
        }
      }
    })
  </script>
  </body>
</html>

General Structure



1

Construction

How is the style tree constructed? How does Descartes prepare any inputs so they can be applied as styles?

2

Flattening

The next step is taking the style tree and turning it into something workable for JavaScript to use.


3

Painting

This is where the magic happens: cascading, style application, and event binding.

4

Exceptions

There's a bunch of other cool parts to this that are worth mentioning. These are the special exceptions.

Descartes.add()


This is the typical entry point here. What this does is two things: a merge and the meat of the engine render(). At a high level, add() lets you throw in any valid style tree and merges it in with any other style trees that currently exist in Descartes. render() is what processes the style tree and all the magic.

/**
   * Adds another style tree to the existing tree and renders
   * @param {object} tree - the style tree to be added
  */
  add(tree) {
    this.styles = this.merge(tree)
    this.render()
  }
/**
   * Merges a style tree with another tree
   * @param {object} tree - the style tree to be merged in
   * @param {object} target - the target style tree, sensibly defaults to this.styles
   * @return {object} the resulting merged tree
  */
  merge(tree, target = this.styles) {
    if (typeof tree !== 'object') return target
    if (Object.keys(tree).length === 0) return target
    let result = Object.assign({}, target)
    for (let key in tree) {
      if (tree.hasOwnProperty(key)) {
        let subtree = tree[key]
        if (target.hasOwnProperty(key)) {
          let targetSubtree = target[key]
          let treeType = typeof subtree
          if (treeType === typeof targetSubtree) {
            switch (treeType) {
              case 'object':
                result[key] = this.merge(subtree, targetSubtree)
                break
              case 'string':
                result[key] = subtree
                break
              case 'array':
                result[key] = subtree.concat(targetSubtree)
                break
              default:
                console.error("Merge failed.")
            }
          } else {
            let targetType = typeof targetSubtree
            if (this.isProperty(key)) {
              result[key] = subtree
            } else {
              console.error("Merge failed.")
            }
          }
        } else {
          result[key] = subtree
        }
      }
    }
    return result
  }

Descartes.render()


render() is where the magic happens. At a high level, this takes the style tree that Descartes knows about using add() and applies all the styles accordingly. You can see the major functions here:

/**
  * Based on the style tree passed to the engine, applies all styles
  */
  render() {
    this.flatten()
    this.bindAliases()
    this.cascade()
    this.paint()
    this.bindListeners()
  }

Descartes.flatten()


flatten() takes your deep style tree that is represented as an object literal and turns it into a "flat" object stored in Descartes.mapping with the DOM selector as a key and all the information about that selector as a value. It would do this:

{
  "html": {
    "margin": 0,
    "padding": 0,
    "body": {
      "background": "#fff",
      "p": {
        "font-size": 12
      }
    }
  }
}
{
  "html": {
    "priority": 0,
    "alias": null,
    "rules": {
      "margin": 0,
      "padding": 0
    }
  },
  "html body": {
    "priority": 1,
    "alias": null,
    "rules": {
      "background": "#fff"
    }
  },
  "html body p": {
    "priority": 2,
    "alias": null,
    "rules": {
      "font-size": 12
    }
  }
}

Descartes.mapping


With a proper Descartes.mapping after flatten(), all we have to do now is loop through each of the keys and do the heavy work: alias binding, cascading, and style application:

{
  "html": {
    "priority": 0,
    "alias": null,
    "rules": {
      "margin": 0,
      "padding": 0
    }
  },
  "html body": {
    "priority": 1,
    "alias": null,
    "rules": {
      "background": "#fff"
    }
  },
  "html body p": {
    "priority": 2,
    "alias": null,
    "rules": {
      "font-size": 12
    }
  }
}

Descartes.bindAliases()


Alias binding is pretty straightforward. Basically, you are setting the properties of an object in Descartes.alias to be functions you can use as callbacks:

/**
   * Creates aliases for listener functions
  */
  bindAliases() {
    for (let selector in this.mappings) {
      let mapping = this.mappings[selector]
      let alias = mapping.alias
      if (this.alias.hasOwnProperty(alias) && alias !== null) {
        console.error("An alias of the name '" + alias + "' already exists")
      } else {
        this.alias[alias] = {}
        let listeners = {}
        for (let property in mapping.rules) {
          let value = mapping.rules[property]
          if (typeof value === 'function') {
            let ruleset = {}
            ruleset[property] = value
            listeners[property] = () => {
              this.applyRuleset(selector, ruleset)
              this.paint()
              return true
            }
          }
        }
        this.alias[alias] = listeners
      }
    }
  }

Descartes.cascade()


Going back to the style application portion, cascade() goes through Descartes.mapping and tries to create an ordered list of rules to apply based on the rules' priority. This essentially mimics what CSS cascading does.

  /**
   * Prioritizes and cascades the style tree for the entire document
  */
  cascade() {
    let prioritizedList = Array.apply(null, Array(this.mappingsPriority + 1)).map(() => { return [] })
    for (let key in this.mappings) {
      let mapping = this.mappings[key]
      prioritizedList[mapping.priority].push([key, mapping.rules])
    }
    prioritizedList.map(set => {
      set.map(mapping => {
        this.applyRuleset(mapping[0], mapping[1])
      })
    })
  }

After the rules are prioritized (or cascaded overall), applyRuleset(selector, rules) is fired in the order of their priority.

Descartes.applyRuleset()


At this point, Descartes starts interacting with the DOM. Using Sizzle, it tries to get the relevant DOM elements for the rule using find() and creates a data-descartes attribute for each element. As the rules get applied, that data-descartes attribute keeps getting built up until the final ruleset for that element is constructed.

/**
   * Apply a ruleset for a certain selector into the selector's nodes
   * @param {string} selector - the selector string i.e. "html body .thing"
   * @param {object} ruleset - the full style ruleset to be applied
  */
  applyRuleset(selector = null, ruleset = null) {
    if (selector === null || ruleset === null) return false
    if (this.hasPsuedo(selector) && this.applyPsuedo(selector, ruleset)) return true
    let elems = this.find(selector.toString())
    if (elems.length === 0) return false
    elems.map(elem => {
      let style = elem.getAttribute('data-descartes')
      if (typeof style === 'undefined') return
      style = (style === null) ? {} : JSON.parse(style)
      let computed = {}
      for (let property in ruleset) {
        computed[property] = this.computeRule(property, ruleset[property], elem)
      }
      style = Object.assign(style, computed)
      elem.setAttribute('data-descartes', JSON.stringify(style))
    })
  }

If you look at the HTML elements for the source code, you'll see that data-descartes attribute there when you are debugging, and even watch it change for any elements that you bound an event to using Descartes.alias

Descartes.computeRule()


It's also mentioning that computeRule() does some really cool stuff here like evaluating functional values in properties, doing pixel inference, and string escaping.

/**
   * Apply a ruleset for a certain selector
   * @param {string} property - the name of the property i.e. "border", "margin", etc.
   * @param {object} value - the unparsed value of the rule, a function, string, or number
   * @param {object} elem - the DOM element that the value function should use, if passed
   * @return {string, bool} the valid CSS property value, otherwise false
  */
  computeRule(property, value, elem = null) {
    // If the value is a function, evaluate the function to get the computed value
    if (typeof value === 'function') {
      try {
        value = value(elem)
      }
      catch(e) {
        return false
      }
    }
    if (value === null) return null
    if (value === undefined) return null
    let except = this.pxExceptions
    if (Number(value) === value && except.indexOf(property) < 0) {
      return value.toString() + "px"
    }
    if (property === 'content') {
      return "'" + value.toString() + "'"
    }
    return value.toString()
  }

Descartes.paint()


Finally, with all the cascaded, computed values set in each of the elements, paint() fires which goes through all the elements and applies the style attribute using the information in data-descartes:

/**
   * Apply inline styles for all finalized rules
  */
  paint() {
    let all = this.find("*")
    all.map(x => {
      let style = x.getAttribute('data-descartes')
      if (typeof style === 'undefined' || style === null) return
      x.setAttribute('style', this.createStyleString(JSON.parse(style), x))
    })
  }

Descartes.createStyleString()


An interesting detail here is how createStyleString() also uses computeRule() to do the actual string construction.

/**
   * Generate valid CSS ruleset as a string
   * @param {object} ruleset - a full ruleset to be converted
   * @param {object} elem - the DOM node to evaluate any functional values on
   * @return {string} the final CSS ruleset string
  */
  createStyleString(ruleset, elem = null) {
    let style = ""
    for (let property in ruleset) {
      let value = ruleset[property];
      let computedRule = this.computeRule(property, value, elem)
      if (computedRule) style += property + ": " + computedRule + "; "
    }
    style = style.slice(0, -1);
    return style
  }

Extras



1

DOM Hiding

The first thing that Descartes does is actually hide the entire DOM until the first initial paint is fired, so there's no flashing.

2

Stylesheet

With createStyleString() there is also a way to create an actual stylesheet to use as a backup.


3

Compatibility

Because Descartes is just scoped to actual styling but is transparent about event binding and paint control, it plays nice.

4

Extensibility

You can start to see some really cool entry points to do prefixing, custom properties, etc.

4

What's Next

Things rockin' my noggin

Ideas



1

Performance

The number one thing on my mind is how to speed up Descartes. Workers, backups, and hardware acceleration are all on my mind.

2

Ecosystem

Could Descartes open up the door to allow for custom rules, libraries, and other extensions built on it?


3

Event Binding

I've wavered back and forth on this so many times, but what's the scope here? Things like responsiveness seem to be in that space.

4

Cascading

I don't think cascading is perfect here, but could it be improved without using specificity as the guiding principle?

Thanks

jonhmchan



Still experimenting

I'm really looking for feedback, even really crazy ones.

Contribute on GitHub

If this really interests you, take a look at the code.

Follow me on Twitter

I would love to keep the conversation going

jonhmchan