15 Oct 2017
I've seen CSS get out of control. It's Real Programming, and it's worth putting some time into thinking about how to structure it upfront.
There are lots of ways to do it. Here's, generally, how I do mine.
My two goals are:
These goals are, to some extent, in conflict.
Right now I'm generally using Bootstrap. It's heavy, but it's well-known and therefore maintainable.
SCSS - powerful and Rails default.
I like BEM.
The short version:
block__element--modifiername--modifiervalue
contact-page__email-button--theme--ocean
contact-page__email-button
is completely unrelated to quote-page__email-button
.contact-page__email-form__submit-button
- instead do contact-page__submit-button
. Or if that doesn't make sense, make a new block (but don't show the nesting in block names).While BEM does a good job of making styling obvious, the problem is avoiding repetition.
I see styling as a three-step mapping:
"Meaning of information" —> “Type of information” —> “Visual style”
For example:
Many different Meanings might have the same Type, here - on the same site, the Blogpost's Date, a Category's Description, and so on might all need to look consistent.
Naming them by Visual Styles would give us:
<div class='light-grey-text'>Gwyn Morfey</div>
That's only one refactor away from ending up as:
.light-grey-text {color: red;}
So we're not doing that.
Naming them by Types would be more sensible:
<div class='secondary-info'>Author: Gwyn Morfey</div>
<div class='secondary-info'>Date: 14 Oct 2017</div>
...
.secondary-info {
font-size: 9pt;
color: gray;
}
But BEM doesn't allow this. Using Meanings gives us:
<div class='author'>Author: Gwyn Morfey</div>
<div class='date'>Date: 14 Oct 2017</div>
...
.author {
color:gray;
font-size: 9pt;
}
.date {
color:gray;
font-size: 9pt;
}
This is repetitive, and it'll get out of sync. If we fixed it using an OR selector in the CSS definition, we'd get:
.author, .date {
color:gray;
font-size: 9pt;
}
Which is better, but can lead to non-obvious results if we also want to have some styles that apply only to .author - we'll end up with multiple sets of rules that apply to one class.
I handle this by separately defining my Types in a utility file, and then using SCSS's @extend functionality to bring them in to the Meaning-based classes:
.secondary-info {
color:gray;
font-size: 9pt;
}
.author {
@extend .secondary-info;
}
.date {
@extend .secondary-info;
}
The starting point is total consistency - the heading hierarchy should be clear, with h2 looking the same everywhere across the site. But there may be places (eg a sidebar) where h2 is semantically correct, but needs to look different.
I agree that using .h1, .h2 etc for typography classes could be better: <h1 class='h2'>
is not a bug but it looks like one.
.t1, .t2, .t3 makes sense to me, but in small projects where I never need to override headings, I think it also makes sense to just use h1 directly (breaking BEM, but in a readable way).
I generally don't use it (see Don't Be Clever). I currently make an exception by nesting elements inside blocks - although the generated CSS does have different specificity, it doesn't break anything and it makes the SCSS easier to read. For example:
.about-me {
max-width:600px;
.about-me__name {
font-weight:bold;
}
}
CSS is not code golf. It's better to be explicit, even if that means being verbose. BEM implements this as "always select on classNames, not element names", let alone on esoteric stuff like parents, siblings, children, roles, &c...
... unless the alternative is worse. For example, on this site, I use larger bottom margins on <p tags inside article bodies, to make large blocks of text easier to read. A strictly BEM approach would be:
<div class='note'>
<p class='note__para'>
A paragraph
</p>
With CSS like:
.note_para {
margin-bottom: 1.1em;
}
But doing that would involve hacking the markdown renderer to add that class to every P (and later to Li, H2, &c). In this case it's better to carve out an exception, and do:
.note__body { /* Yup, breaking BEM a bit here */
li, p {
margin-bottom:1.1em;
}
h2,h3,h4 {
margin-top:1.7em;
margin-bottom:1em;
}
I do it, because @extend makes it easy. For example, I do:
<input class='button--yellow'>
.button--yellow {
@extend .button
color: 0;
}
Rather than:
<input class='button button--yellow'>
The BEM standard uses kebab-case for block names. Rails prefers snake_case, because foo-bar isn't a valid variable name in ruby. Where I'm using a partial for a block, I keep the name consistent but map the naming style, so _note_summary.html.erb would contain:
<div class='note-summary'>
<h1><%= note_summary.title %></h1>
</div>
SCSS's @mixin system is awesomely powerful, including parameters, conditionals and even an equivalent to Ruby's blocks. There's a good overview here.
npm install -g sassdoc
sassdoc app/assets/stylesheets/ && open sassdoc/index.html
The docs are extensive, but the short version is: describe things with comments beginning with / . Eg:
/// Typography style. Used for text that's secondary, like
/// metadata (author name, date), sidebar info, etc.
.secondary-info {
font-size:70%;
color:$gray;
}
When making decisions, I always like to select the best option, and reject the others, and record why. This saves time later, since I'm not tempted to switch back later on, having forgotten why the other option is worse.
For naming conventions, I looked at a number of others. Here's a good overview. SMACSS seems to have some traction, but according to one example:
// CSS
.questionCard {
position: relative;
margin-top: $ scale1;
padding: $ scale2;
background-color: ;
box-shadow: $ boxShadow-2;
}
// html
<div class="questionCard">...
becomes:
// CSS
.u-relative {
position: relative;
}
.u-mt1 {
margin-top: $ scale1;
}
.u-p2 {
padding: $ scale2;
}
.v-bg-white {
background-color: ;
}
.b-bs2 {
box-shadow: $ boxShadow-2;
}
// HTML
<div class="u-relative u-mt1 u-p2 v-bg-white b-bs2">...
At least to my eye, that's worse; it's mixed the presentation into the HTML, and it's more verbose and harder to read. Perhaps it's intended for different types of project.
So far I've found this to be overkill, although I can see the value of separating 'position' and 'appearance'. I do use classnames beginning 'js-' for, and exclusively for, javascript hooks.
There's an argument that a (Vuejs or React, or similar) Component should incorporate its own CSS along with HTML and JS. I'm not entirely convinced by this yet, although I could see it working if used with SCSS variables, @mixins and @extend. Currently my standard is not to do this.
Gemfile:
gem 'bootstrap-sass'
app/assets/stylesheets/application.scss:
@import url("https://fonts.googleapis.com/css?family=Arvo");
@import "_variables"; /* Must do this before loading bootstrap */
@import "bootstrap-sprockets";
@import "bootstrap";
@import "_utilities";
@import "_fixes";
@import "blocks";
app/assets/stylesheets/_variables.scss:
$page-bg-color: ;
$text-color: #222;
$font-family-base: Arvo, serif;
$font-size-base: 20px;
$font-size-h1: $font-size-base * 2.4;
$font-size-h2: $font-size-base * 1.5;
$font-size-h3: $font-size-base * 1.25;
$font-size-h4: $font-size-base * 1;
$font-size-h5: $font-size-base * 1;
$font-size-h6: $font-size-base * 1;
$brand-primary: #303535;
$brand-verylight: lighten($brand-primary, 50%);
$navbar-inverse-bg: darken($brand-primary, 25%);
$navbar-inverse-color: ;
$navbar-inverse-link-color: ;
app/assets/stylesheets/_utilities.scss:
/* These are mixed in to blocks with @extend, not invoked directly */
.secondary-info {
font-size:70%;
color:$gray;
}
app/assets/stylesheets/blocks.scss:
/* Wrapper ----------------------- */
.wrapper-content {
max-width: 800px;
margin-left:auto;
margin-right:auto;
}
/* Blocks used only on the homepage ----- */
.about-me {
@extend .secondary-info;
max-width:600px;
.about-me__name {
font-weight:bold;
}
}
.note-summary {
padding-bottom: 0.5em;
padding-top: 0.5em;
padding-left: 0.2em;
padding-right: 0.2em;
border-bottom: 1px dotted $gray-light;
min-height: 6em;
.note-summary__date {
@extend .secondary-info;
}
}
.category-link {
@extend .note-summary;
background-color: $brand-verylight;
min-height: inherit;
text-align:center;
.category-link__link {
}
}
/* Blocks used only on note view page ------- */
.note {
.note__title {
}
.note__date {
@extend .secondary-info;
text-align:right;
margin-bottom:1em;
}
.note__body { /* Yup, breaking BEM a bit here */
li, p {
margin-bottom:1.1em;
}
h2,h3,h4 {
margin-top:1.7em;
margin-bottom:1em;
}
a {
text-decoration: underline;
}
}
}
.more-notes {
@extend .secondary-info;
text-align: center;
margin-top:4em;
padding-top:1em;
margin-bottom:2em;
border-top:1px solid $gray;
}