Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Last active November 13, 2018 15:47
Show Gist options
  • Save jamesarosen/2f7d057e0cd69334f670b96c5cb96598 to your computer and use it in GitHub Desktop.
Save jamesarosen/2f7d057e0cd69334f670b96c5cb96598 to your computer and use it in GitHub Desktop.
On CSS

I don't have a Grand Vision for (S)CSS, but I do have some ideas.

Cohesion & Coupling

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:

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}}
  some content
{{/my-widget}}
.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:

{{#my-widget class='layout-row layout-row__narrow bg--grey fg--red'}}
  some content
{{/my-widget}}
.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 data-width=small>
  {{#my-widget class='bg--grey fg--red'}}
    some content
  {{/my-widget}}
</layout-row>
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.)

Separate Layout

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.)

Case Study: Notifications

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.

Contextual Override

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).

Defined Override Class

// 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.

Defined Override Class, Color Utility

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.

// authentication.hbs:
<div class='bg--red'>
  <div class='notification notification--success--inverted'>
    You did it!
  </div>
</div>

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.

Defined Override Mixin

// 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.)

Mythical If

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.

Predefine Contexts

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:

<div class='bg--red'>
  <div class='notification--success fg--blue'>
    <a href='https://www.youtube.com/watch?v=vcPIA_E6AXg' target='_blank'>
      Yo listen up here's a story
    </a>
  </div>
</div>

CSS Variables

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

SCSS Themes

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.

Conclusion

There is no conclusion. That scares me. But at least it's honest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment