Or, how to (d)evolve from convoluted coding conventions to simpler rule definitions.
CSS is 20 years old, hooray!
And we're still struggling with the same old problems of CSS at scale. Boo.
Things are changing, though. To be fair, the spec for a real change came a while ago already, but this kind of things take their time to become standard and widespread.
The problem
The biggest concern about Cascading Style Sheets is... the cascade. In short, a plethora of rules that could all influence each other, also because they've all a global scope. It soon became evident that things like these don't work very well:
Code | Effect |
---|---|
.content { ... } |
"Wait, I didn't add this border radius here, what's happening?" "Oh. Oh, yeah, I've already defined a content class somewhere else, sorry." |
#login .header { ... } |
"Why is this all unstyled?" "Yeah, well, you have to keep that login ID" "But this isn't the login page!" "Uh, well, I guess..." |
article > section > p { ... } |
"This isn't the right style for the paragraph, why?" "See, I had to wrap the section in a div here..." |
z-index: 99999999999; |
"How many 9's should I put here again?" |
color: #fff !important; |
"Great, now I can't change the color here!" |
-webkit-text-fill-color: #000; |
"Seriously, dude?" |
Name clashing. Specificty races. Unimportant declaration marked as !important
out of frustration. Styles that spill everywhere. Even differences among the browsers didn't help. At all.
The first solutions
As web pages and applications grew in complexity, some best practices emerged:
- no more ID's in selectors;
- no "brute-force" values (as that
z-index
); - no more
!important
or exotic CSS properties; - no strict bindings between CSS and the DOM structure;
- keep the specifity as low as possible, ideally 0.1.0;
- use namespaces for your classes.
It still wasn't enough, so CSS - the simplest of all web languages - got its coding style guides too. Conventions like OOCSS, SMACSS, Atomic CSS and BEM were born. The last is arguably the most successful one, so I'll refer to it during the article, although the same concepts apply more or less for the others.
Scoping the style
Let's say we have our card prototype:
And we back this design using this HTML structure:
<div class="content-card">
<header class="content-card__header">...</header>
<picture class="content-card__picture">
<img src="..." class="content-card__image">
</picture>
<section class="content-card__abstract">
<p>...</p>
</section>
<footer class="content-card__footer">
<button type="button" class="content-card__favorite">☆</button>
<nav class="content-card__actions">
<a href="#" class="content-card__action">Read later</a>
<a href="#" class="content-card__action content-card__action--view">View</a>
</nav>
</footer>
</div>
Behold the code in all its glorious BEM verbosity!
Using BEM allowed us to name our classes clearly, without caring (much) about name clashing, specificity headaches and all the rest. But it came at the cost of lengthy class names, not always easy to read. Even using preprocessor rule nesting tricks like this one has its disavantages:
.content-card {
...
&__action {
...
&--view {
...
}
}
}
Now, if I want to track down the rule for the class content-card__action
, I can't just copy the name and paste in the search box of my editor of choice. Not really a deal breaker but still annoying.
Scoped styles
What we really need is to have a consistent - possibly native - system to apply our styles locally. BEM shows the way to do it but it's still a manual operation, which means it's prone to errors.
One of the first native approaches (back in 2012) to the problem came in the form of the scoped
attribute of <style>
elements:
<div>
<style scoped>
div { width: 30em; }
p { color: #333; }
<style>
<p>I look black but I'm actually nicer</p>
</div>
<p>I'm pitch black</p>
Thanks to that, the rules defined inside affect only on the <style>
's parent and all of its descendants. While it seemed cool and right on track, it didn't gain much momentum (Chrome removed its support on v36), because it didn't solve the other, big problem of shielding our styles from external influences.
In short, global and parent scoped style rules can still override our local ones.
The (almost) definitive solution
When web development became aware that we need to split our interfaces into components and develop with this concept in mind, the path was set - although not easy to follow.
Web Components are a concept that actually predates scoped styles, as they've been introduced in 2011. But they had a long journey before a decently widespread adoption, and also API maturity. But together with the concept of Shadow DOM, it came the encapsulated style sheet.
This means that not only our component's style sheet with have a local effect, but also that external one will have no effect to our component!
The main implication of this is that we have no need for a namespace for our classes, as there's no risk of name clashing or style spilling anymore. In short, we can reduce the HTML for our picture card like this:
<div class="content-card">
<header class="header">...</header>
<picture class="picture">
<img src="..." class="image">
</picture>
<section class="abstract">
<p>...</p>
</section>
<footer class="footer">
<button type="button" class="favorite">☆</button>
<nav class="actions">
<a href="#" class="action">Read later</a>
<a href="#" class="action action--view">View</a>
</nav>
</footer>
</div>
That's already much better! But we can do even more if we fully take advantage of the encapsulation. This means that we can also ditch the convention of using only selectors of specifity 0.1.0 for our rules.
Should we avoid using classes, then? Not really: we can use them only when necessary. Because not only our style sheets are now encapsulated, but the whole concept of componentization of our interface naturally brings the habit of writing small things: small JavaScript modules, small HTML and small CSS.
This implies that our semantically chosen HTML elements are possibily the only ones in the whole component: we'll presumably have only one header
, one footer
, or maybe multiple li
but for just one type of element.
In the end we can shrink our markup even further:
<content-card>
<header>...</header>
<picture>
<img src="...">
</picture>
<section>
<p>...</p>
</section>
<footer>
<button type="button">☆</button>
<nav>
<a href="#">Read later</a>
<a href="#" class="view">View</a>
</nav>
</footer>
</content-card>
Now this is even better, readable and clear. We even shaved some meaningful bytes from the payload. And if the semantic meaning (for the developers) has vanished, we can use ID's again, as they're encapsulated too.
But I want to alter a component's appearance from the parent!
It's a legitimate need, no doubt. Unofrtunately this issue still lacks a definitve solution.
There has been a proposal to introduce a "shadow-piercing" descendant combinator in CSS (the >>>
or /deep/
combinator), but has been deprecated since then as it's been considered as "too powerful" for the intent. Indeed, it would have reduced CSS encapsulation to mere scoped CSS.
The only way to change a custom element's appearance is by using CSS "variables" (or better "custom properties"):
/* styles.css */
:root {
--base-button-color: rebeccapurple;
}
/* base-button's style */
button {
background-color: var(--base-button-color, midnightblue);
}
This is a little bit inconvenient as it's deemed too "fine-grained", which also could lead to name clashing (again!) when it comes to naming said custom properties. This is why Tab Atkins proposed the @apply
rule, that essentially meant mixins in CSS. While that sounded great, it didn't solve other problems in the matter and the proposal has been abandoned by its champion and we won't probably see it spec'ed.
The linked article mentions the ::part
and ::theme
pseudo-elements that could finally solve the problem, but there's still quite some road to walk. In the end, we have to stick to custom properties for now.
How can I use encapsulated CSS though?
Web Components have a complex API that might put off some developers. That's why a library like Polymer has been born. Even if Polymer or even Web Components are out of question for your development needs, there are frameworks that let you take advantage of encapsulation for styles.
When Web Components or even just Shadow DOM are ruled out, style encapsulation can be emulated. This usually means generating an attribute with a random name for each component, and attach it to every style rule and DOM element of the component. So, starting with the following markup and style sheet:
<div>
<h2>Hello!</h2>
<p>I will get a random attribute even though I'm unstyled</p>
</div>
h2 { font-size: 150%; }
They will be translated into something like this when processed:
<!-- Inside the <head> -->
<style type="text/css">
h2[data-v-4c74d97c] { font-size: 150%; }
</style>
<!-- In the page -->
<div data-v-4c74d97c>
<h2 data-v-4c74d97c>Hello!</h2>
<p data-v-4c74d97c>I will get a random attribute even though I'm unstyled</p>
</div>
Another component will get a different generated attribute, and that's how style rules get scoped.
Emulating style encapsulation is slower when it comes to applying the styles, but it won't probably affect your page significatively. So let's see how the most common front-end libraries handle style encapsulation.
Vue
Vue per se is agnostic when it comes to CSS, but Vue as an ecosystem offers vue-loader, a loader for Webpack that leverages on a PostCSS plugin to achieve emulated encapsulation. The loader gets included using vue-cli with a Webpack-based scaffolding template.
In order to encapsulate your styles, you have to define a component's style using the <style>
with the scoped
attribute. Now this is a bit inconvenient, as it's the same syntax from the old spec for element-scoped styles, without having the same effect.
In fact, descendant components will not be styled by the scoped style, opposed to the original proposal of <style scoped>
, because they will miss the generated attribute defined for the ancestor. But still, encapsulation.
Angular
Angular is more sophisticated when it comes to encapsulation, as it let us decide among no encapsulation at all, emulated (which is the default one) and even taking advantage of Shadow DOM's native encapsulation. I guess this is possible also because Angular does not replace the host element from the page, de facto replicating what Web Components do.
React/Preact
Although React and its lesser known (but not less powerful) alternatives like Preact don't offer native solutions for CSS, there's a plethora of libraries that allow to style our components, from CSS Modules to Styled Components, from Glamor to JSXStyle.
These libraries are all part of the great CSS-in-JS subject, and they all are based on the concept of encapsulated style, although mostly based on an emulated system.
Conclusions
CSS comes from a long way. It's probably one of the simplest language around, it's simple to use but this doesn't mean it's simple to manage. With the advent of large-scale web applications, the issue became evident, but solutions didn't.
After long, struggling years, with style encapsulation we've managed to scale down the problem dramatically. Not only it seems a step in the right direction, but it feels like it. So it doesn't matter what library will you use for your next project, the advice is to choose one that allow style encapsulation.
Top comments (0)