I don't have a Grand Vision for (S)CSS, but I do have some ideas.
In JavaScript (and any other "programming language"), we value cohesion and eschew coupling. To value cohesion is to say that all of the foo-related things are in the Foo
component or the app/pods/foo
pod or the lib/foo
addon. Like things are together. To eschew coupling is to say that two unrelated things should not need to know about one another. Unlike things don't rely on one another. Further reading on Cohesion & Coupling:
- http://courses.cs.washington.edu/courses/cse403/96sp/coupling-cohesion.html (my favorite reference on the topic)
- http://enterprisecraftsmanship.com/2015/09/02/cohesion-coupling-difference/ (good visuals)
- http://c2.com/cgi/wiki?CouplingAndCohesion
- https://en.wikipedia.org/wiki/Connascence_(computer_programming) (connascense is closely related to cohesion)
We can apply this to SCSS. The following examples are in decreasing order of coupling (improving).
In the first example, we have a single class that does three (or more, depending on what $my-container
does, which is probably a lot) things:
.my-widget {
@include my-container(33rem);
background-color: $corporate-grey;
color: $corporate-red;
}
In the second, we break the single class into multiple utility classes, each of which wraps a single concept:
.layout-row { display: block; }
.layout-row__narrow { @include max-width(33rem); }
.bg--grey { background-color: $corporate-grey; }
.fg--red { color: $corporate-red; }
In the third, we actually break up the DOM into pieces:
layout-row {
display: block;
&[data-width=small] { @include max-width(33rem); }
}
.bg--grey { background-color: $corporate-grey; }
.fg--red { color: $corporate-red; }
Notice that I didn't break this into three elements (one for layout, one for foreground, and one for background). That's because the bg--grey
and fg--red
are highly correlated (because of contrast rules and aesthetics). But class='bg--grey fg--blue'
is equally valid, so we don't really want to combine the two into one concept. Any default we picked would get overridden as often as it applied. (class='bg--grey fg--grey'
is probably not valid, and we might want to write CSS rules that make this so obviously ugly that we don't accidentally do it.)
Notice also that in the third example I put the <layout-row>
outside the my-component
. That is because of a rule I stole from Chris Thoburn's Talk, CSS is Impossible: components always take up their full parent's width.
If we want a {{my-input}}
that's narrow, wrap it in a <layout-narrow-column>
or something similar. (In uncommon, but not impossible cases, a particular DOM structure is required, so it might make sense to make some of these rules available as classes or do something like {{my-input tagName='layout-narrow-column'}}
. At a minimum, express the underlying values as SCSS variables so one-off classes can use them.)
Years ago, there was a movement in CSS to make all the class names semantic. The argument was that if our markup looks like
<div class='bg--green fg--white'>
You did it!
</div>
then it's going to be hard to change all our success notifications from white-on-green to white-on-blue. And so they said we should instead write
<div class='success-notification'>
You did it!
</div>
.success-notification {
background-color: green;
color: white;
}
then that migration becomes trivial. And they're right!
Yet...
If we do something like this:
.notification {
border: 1px solid currentColor;
border-radius: 4px;
padding: 8px;
}
.notification--success {
background-color: green;
color: white;
}
then it's very hard to reuse a success notification in a slightly different context, like in an area of the site where the body is red instead of white.
I can think of a few solutions to this.
We override the CSS as-needed:
// notification.scss:
.notification--success {
background-color: green;
color: white;
}
// authentication.scss
.authentication {
background-color: red;
.notification--success {
background-color: white;
color: green;
}
}
// billing.scss
.billing {
background-color: red;
.notification--success {
background-color: white;
color: green;
}
}
There's lots of repetition, which is bad for developers and bad for download time. But one nice thing is that the fact that the .notification--success
needs to change is right next to why it needs to change (the surrounding color).
// notification.scss:
.notification--success {
background-color: green;
color: white;
}
.notification--success--inverted {
background-color: white;
color: green;
}
// authentication.hbs:
<div class='notification notification--success--inverted'>
You did it!
</div>
Now sections that want inverted notification colors can get them without having to repeat themselves. But we've introduced a new problem: the fact that Authentication uses an inverted notification is in authentication.hbs
, but the reason it does is in authentication.css
.
We could move both the background-is-red knowledge and the notifications-are-inverted knowledge into the template:
.bg--red { background-color: red !important; }
Read Chris Coyier's When Using !important is the Right Choise for more on this utility class.
Now we're very clearly mixing two concepts -- content and coloring -- in one file. But there are some distinct pros. First, we've kept the notification-inversion knowledge next to the contextual color. Second, we've defined a clear API between CSS and HTMLBars; the implementation is in CSS, but the decisions about where the API is used are in Handlebars. It's a clear one-way client-to-provider connection.
// notification.scss:
.notification--success {
background-color: green;
color: white;
}
@mixin invert-notification-colors {
.notification--success {
background-color: white;
color: green;
}
}
// authentication.scss:
.authentication {
background-color: red;
@include invert-notification-colors;
}
This seems like a pretty good balance: we have high cohesion about what a notification looks like, but we distribute the knowledge of variations to the relevant context. But it introduces a new problem: performance. Each @include
generates the equivalent of the "Contextual Override" case. More bytes means longer boot times. (Though I don't have a good intuition as to how many bytes are too many bytes.)
It would be lovely if CSS let us do something like
// notification.scss:
.notification--success {
if (parent.background-color darkerThan #777) {
background-color: white;
color: green;
} else {
background-color: green;
color: white;
}
}
Element Queries or Container Queries are the right idea, but I can't find any examples that do background-color checks.
If we only ever did background coloring via utility classes (and that's a big if), we could do something like
// notification.scss:
.notification--success {
background-color: green;
color: white;
}
$dark-backounds: bg--blue, bg--green, bg--grey, bg--grey--dark, bg--purple, bg--purple--dark;
@each $bg in $dark-backounds {
.#{$bg} .notification--success {
background-color: white;
color: green;
}
}
This has high cohesion around notifications since all the knowledge about how and why to invert is in one place. (Though over time there will of course be new background colors, which means $dark-backgrounds
has to be with all the other background-color information, so we have a new micro-API.) Because of this, we don't have to know to add class='notification--success--inverted'
each time we use a bg--*
utility class.
And since we're using utility classes with !important
, it's not too hard to override this default behavior:
We can fix the duplicate-CSS problem of Contextual Override by using CSS Variables:
// notificaiton.scss:
.notification--success {
background-color: green;
background-color: var(--notification--success--bg-color);
}
// authentication.scss:
.authentication {
--notification--success--bg-color: white;
}
Unforutnately, browser support is still quite weak (as of July 2017). Using per-context overrides means more duplication but also more flexibility. An alternative would be to have theme overrides. There are several ways to do this in SCSS. One is to have a mixin:
// themes/red-page.scss:
@mixin red-page() {
--page--bg-color: red;
--notification--success--bg-color: white;
}
// pages/authentication/scss:
.authentication() {
@include red-page;
}
It would be possible to do the same with theme utility classes, as in
.theme--red-page {
--page--bg-color: red;
--notification--success--bg-color: white;
}
<div class='authentication theme--red-page'>
Of course, none of this works now
While we're waiting for browsers to support CSS variables, we could use SCSS to define our themes. Erik Bryn suggests something like this in his Taming CSS in Ember.js Apps. That would look something like
// components/notification.scss:
@mixin notification--success($background-color: green, $color: white, $icon-color: darkgreen) {
.notification--success {
background-color: $background-color;
color: $color;
.icon { color: $icon-color; }
}
}
// themes/default.scss:
include $notification-success();
// themes/red.scss:
include $notification-success($background-color: white, $color: darkgrey);
One of the really nice aspects of this compromise is that it lets us upgrade to the CSS Variables version cleanly. We can upgrade a component at a time and upgrading happens all in one place (the definition of the component). The contract between component and theme remains the SCSS @mixin
. The implementation becomes CSS Variables.
There is no conclusion. That scares me. But at least it's honest.