CSS: A New Kind Of JavaScript
Originally published on Medium.
Those familiar with the web platform will be well-versed in its two complementary technologies: HTML for documents and their structure, and JavaScript for interaction and styling.
For as long as anyone can remember, styling documents — affecting their appearance — has been facilitated via the JavaScript style
property, which is exposed for any supporting DOM node.
node.style.color = 'red';
Before the advent of this styling API, HTML authors had to write style attributes into the DOM manually, impeding the editorial process.
Combined with JavaScript’s node selection engine, we are able to style multiple elements simultaneously. In the following example, all p
nodes are styled with the red text color.
const nodes = document.querySelectorAll('p');
Array.prototype.forEach.call(nodes, node => {
node.style.color = 'red';
});
One of the great things about this selector engine is it allows one to target different elements simultaneously, by providing a comma-separated list.
const nodes = document.querySelectorAll('p, li, dd');
What’s less straightforward is attaching multiple styles to any one node. The following approach quickly becomes verbose.
node.style.color = 'red';
node.style.backgroundColor = 'black';
node.style.padding = '1rem';
// etc.
The only standardized alternative is to use the cssText
property:
node.style.cssText = 'color: red; background-color: black; padding: 1rem;';
Managing multiple styles as a single string is problematic. It makes it difficult to update, remove, or replace individual styles at a later point in time.
For this reason, authors have invented ways to manage styling information in objects, often by manipulating the Element
interface’s prototype.
Element.prototype.styles = function(attrs) {
Object.keys(attrs).forEach(attr => {
this.style[attr] = attrs[attr];
});
}
Adding styles to a node is now possible like so:
node.styles({
'color': 'red',
'backgroundColor': 'black',
'padding': '1rem'
});
This assignment approach tends to get used a lot throughout an application and its lifecycle. It’s infamously difficult to know where to put all this stuff, or how to clearly separate it from the interaction scripting that is JavaScript’s other responsibility.
But there’s another, more fundamental, problem: these style properties are not reactive. For example, let’s say I’ve set some styles for disabled buttons:
const disableds = document.querySelectorAll('[disabled]');
Array.prototype.forEach.call(disableds, disabled => {
disabled.styles({
'opacity': '0.5',
'cursor': 'not-allowed'
});
});
These styles only apply to disabled buttons that are already in the DOM. Any disabled buttons added to the DOM or — more likely—buttons that acquire the disabled property/attribute, will not automatically adopt the appropriate styling.
button.disabled = true;
button.style // nothing new here
It is possible to listen for the attribute change and react to it using mutationObserver
:
const button = document.querySelector('button');
var config = { attributes: true }
var callback = function(mutationsList) {
for(var mutation of mutationsList) {
if (mutation.type == 'attributes') {
if (button.disabled) {
button.styles({
'opacity': '0.5',
'cursor': 'not-allowed'
});
}
}
}
}
var observer = new MutationObserver(callback);
observer.observe(button, config);
I think we can agree this is quite a lot of code, especially since it only makes one instance of one type of element reactive to one type of attribute change. It also only changes the styling in one direction: we’d have to handle reverting the styles when the disabled property is removed. Not easy, since we don’t know the initial values for opacity
or cursor
.
As much as I like JavaScript, I don’t think it’s very well designed when it comes to styling tasks. It is, after all, a procedural and event-based language, whereas style is just something you have or you don’t.
We waste far too much time writing and maintaining styles with JavaScript, and I think it’s time for a change. Which is why it’s my pleasure to announce an emerging web standard called CSS.
CSS
CSS is a declarative subset of JavaScript, optimized for styling tasks. A CSS file takes the .css
extension and, importantly, is parsed completely separately to standard JavaScript files. With CSS it’s finally possible to separate style from behavior. You can brand your applications without having to touch your business logic!
Syntactic sugar
One of the first things you’ll notice is the cleaner syntax, which fans of CoffeeScript will appreciate:
[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
The object-like structure remains, but you no longer have to explicitly call upon querySelectorAll
to iterate over DOM nodes. The iteration is taken care of internally instead, making it more performant too.
The above example affects all DOM nodes with the disabled
attribute automatically. Better yet: any new buttons adopting the disabled property will immediately adopt the associated styles. Reactive out of the box!
The cascade
CSS stands for Cascading Style Sheets. The cascading part is perhaps its best feature. Consider the following CSS.
button {
background-color: blue;
cursor: pointer;
}
[disabled] {
cursor: not-allowed;
}
The styles for the [disabled]
‘block’ come after the button
block in the style sheet. Any styles declared in the [disabled]
block with the same keys (property names) as those in the preceding button
block become overrides. The upshot is that adding the disabled
attribute/property to a button only updates pertinent styles. In the above example, cursor
is updated but background-color
remains the same. It’s a kind of filtering system.
In addition, where the disabled
attribute/property is removed, the default styles are automatically reinstated — because now the node only matches the button
block further up the cascade. No need to ‘remember’ which styles have been applied and under which conditions.
Resilience
When styling with JavaScript, any unrecognized style properties or syntax mistakes will cease the parsing of the script. Any ensuing styles or interactions will simply be dropped en masse and your whole application will fall over.
CSS is more robust. In most cases, an unrecognized property or a syntax mistake will only cause the single, erroneous declaration (property/value pair) to be dropped.
This innovation acknowledges that different browsers support different styling properties, and that individual styles are not mission critical. The resilience of CSS means more users get access to a functional interface.
Conclusion
A good sign that a technology is not fit for purpose is how much we have to rely on workarounds and best practices to get by. Another sign is just how much code we have to write in order to get simple things done. When it comes to styling, JavaScript is that technology.
CSS solves JavaScript’s styling problems, and elegantly. The question is: are you willing to embrace the change, or are you married to an inferior methodology?
For more information on CSS and tips on getting up and running, read this primer.