CSS Standards and Normalization Part 1: Philosophy
12 Mar 2018 | Josh Boland and Ben RobertsonPurpose
The purpose of this series will be to define some standardized best practices for how CSS is composed and organized in our projects.
This is part 1 of 4: Philosophy. In this article, we will explore our CSS philosophy and provide a high-level overview of how we write css.
Principles
Our CSS philosophy borrows from several different frameworks and systems:
- SMACSS by Jonathan Snook
- The BEM Naming Pattern
- Drupal 8
- Bootstrap
The Goals of Drupal 8’s CSS philosophy serve as a good starting point. Well-architected CSS should be:
1. Predictable
CSS throughout Drupal core and contributed modules should be consistent and understandable. Changes should do what you would expect without side-effects.
2. Reusable
CSS rules should be abstract and decoupled enough that you can build new components quickly from existing parts without having to recode patterns and problems you’ve already solved. – Philip Walton, CSS Architecture
3. Maintainable
As new components and features are needed, it should be easy to add, modify and extend CSS without breaking (or refactoring) existing styles.
4. Scalable
CSS should be easy to manage for a single developer or for large, distributed teams (like Drupal’s).
Source: CSS architecture (for Drupal 8)
In addition, we should keep Mark Otto’s Golden Rule in mind:
Every line of code should appear to be written by a single person, no matter the number of contributors.
Code Guide by Mark Otto
Code Structure & Style
Here’s an overview of how we structure and organize our code (borrowed from css guidelines):
- Sass (not scss)
- Four (4) space indents, no tabs
- Multi-line css
- Meaningful use of white space
- Limit Sass nesting depth to 2 levels
- Limit line length to 80 characters
- Use single line comments
- Clean Import Paths
Sass
Sass is our CSS preprocessor of choice. Sass is bracket-less {} and does not use semi-colons to terminate lines. Overall it provides a cleaner feel to the code.
Each project should include Sass linting. Use our default .sass-lint.yml file. It will point out the rules below for you.
Four space indents
Since we aren’t using brackets, this keeps the Sass clearer and also is aligned with our Javascript and PHP standards. Most code editors can be set up to use four spaces as the default line indent.
Multi-line css
Selectors and properties each get their own line. This makes code legible and easier to parse visually
Do it like this:
.container
width: 95%
max-width: 40em
Not like this (with no brackets in Sass, single-line format is not valid):
.container{width: 95%; max-width: 40em;}
Meaningful use of white space
White space can give context to class definitions. Include two empty lines between top-level css blocks, and one line between properties and nested classes.
Example:
.card
padding: 1rem
margin: 1rem
&__header // leave one empty line above this since it is nested
border-bottom: 1px solid gray
.card--white // two empty lines above this new class block
background-color: white
Limit Sass nesting depth to 2 levels
Nesting depth should be limited to 2. This prevents un-necessary specificity of selectors and makes future maintenance easier. The code is also easier to understand with limited nesting depth.
Example:
.block
&__element
&--modifier // Deepest Nest Allowed
Limit line length to 80 characters
This helps make the code more readable without horizontal scrolling. Because you’re writing multi-line css, this should really only come into play for comments. Limiting your line length will make your comments easier to read and therefore more useful. For instances like long urls or gradient syntax, don’t worry about it. Most code editors also have a setting for maximum line length.
Example:
// This file is where you override default Bootstrap variables. You
// can find a list of the default Bootstrap variables
// in _variables.scss
Use single line comments
Your comments don’t have to be on one single line, just don’t use traditional multi-line comment format, ie: /* These kinds of comments */
,
because they will end up in the compiled css. Your comments, even if they take up multiple lines, should look like this:
// This file is where you override default Bootstrap variables. You
// can find a list of the default Bootstrap variables
// in _variables.scss
Clean Import paths
You don’t need to include leading underscores or filename extensions in your import paths. To stay consistent, your imports should look like this:
@import "base/typography" // where this file is base/_typography.sass
@import "base/colors"
@import "layout/grid"
@import "layout/containers"
Writing Selectors
We have a few preferences when it comes to writing CSS selectors.
- Write selectors for humans
- Use BEM naming for component structure
- Avoid ID selectors
- Avoid Vendor Prefixes
Write Selectors for Humans
Class names should use full words rather than abbreviations.
Remember that your class names are written for the benefit of other developers, not the computer.
Prefer class="block"
to class="blk"
.
Class names for components should use dashes between words for legibility (not underscores).
Prefer class="component-name"
to class="componentname"
.
Class Name Format (Using BEM)
We use a BEM-style class naming system. BEM stands for Block Element Modifier. You can also think about it as Component, Sub-object, Variant. At a high level, BEM seeks to:
- Provide clear and purposeful naming of classes
- Create modular blocks that do not depend on cascading styles
- Avoid inheritance as much as possible
For more on BEM philosophy: BEM Philosophy
More on how we interpret BEM philosophy will be explored in later articles. In general, BEM treats the highest level of a component as a “Block”. A site menu is a good example of a “Block”:
.primary-menu
//some menu styles
Related sub-parts of a “Block” are considered “Elements.” In this example, items in the menu would be “Elements”. Different elements are each given their own unique class name. Element class names use their block’s class name, followed by 2 underscores, then the element’s class name. Elements are NOT nested underneath their block selector to avoid contextual dependency. Like so:
//Like This
.primary-menu
//some menu styles
.primary-menu__menu-item
//styles for the menu item
//Not Like This
.primary-menu
//some menu styles
.primary-menu__menu-item
//styles for the menu item
//this class must now always be a child of .primary-menu for these styles to apply
“Modifiers” or “variants” of elements create slightly different versions of an element. To create a modifier, add two dashes and the modifer name to the end of the element class name. For our menu example, lets say we want some menu items to have a bottom-border.
.primary-menu__menu-item--border-bottom
//styles for the variant menu item with a bottom border
border-bottom: 1px solid #000
If you prefer to leverage Sass’s nesting capabilities, the example above can be written like so:
.primary-menu
//some menu styles
&__menu-item
//styles for the menu item
&--border-bottom
//styles for the variant menu item with a bottom border
border-bottom: 1px solid #000
When using BEM naming, you should not write classes with more than 1 set of double underscores (__). Using the BEM approach, each “Element” gets its own class,
they do not depend on each other. For example, If you had an <a>
tag inside your primary-menu__item
, it may tempting to
add a class of primary-menu__menu-item__link
. This implies element dependency, that the link element depends on being inside the menu item.
Element dependency is something BEM strives to avoid. You would instead add a class to the <a>
element of primary-menu__link
.
// Do this
.primary-menu__menu-item
//menu item styles
.primary-menu__link
//link styles
// Don't do this
.primary-menu__menu-item__link
//nope nope nope
Avoid Using id Selectors
You can use them for Javascript or for providing anchor-links, but don’t use them for styling.
Avoid Vendor Prefixes
For sites that are using a build tool like Grunt or Gulp, we can skip vendor prefixes in the css in favor of using the PostCSS Autoprefixer plugin. It will automatically determine what prefixes are necessary based on browser support requirements and only include the necessary ones. If you build the auto-prefixer into a tooling-chain, don’t do it on every compile during dev, it can slow down your build time.
Use relative units for font-sizing.
Prefer relative units like rem
or em
for font-sizing. This allows for more flexible, more maintainable font styling.
It also allows us to better control font styles based on what fonts have been loaded or not loaded.
Likewise, avoid specifying units for line-height. Line-height should be a ratio of font-size. You will need to write a lot less css if your line-height ratios are on to begin with.
Namespacing
Consider name-spacing layout and javascript specific classes.
For instance, instead of using a class name like .container
, you could use .layout-container
. Or .layout-grid
, over .grid
.
This gives the benefit of clear separation between component-specific and layout-specific styles.
Or for javascript that is manipulating the DOM, use something like .js-behavior-hook
instead of .behavior-hook
to make sure that it is a dedicated class not used for styling.
Source: Drupal 8 CSS Architecture | Class Name Formatting
Linting
The easiest way to put these guidelines into practice will be to use sass-lint
. You can install it globally like so: npm install -g sass-lint
or on a per-project basis like this: npm install sass-lint --save-dev
Add a copy of our .sass-lint.yml to the root of your project.
To lint your project from the command line, you can do this:
sass-lint -v
Or, for as-you-go linting, install the sass-lint plugin for your respective editor:
Resources
We leaned heavily on the following resources for these guidelines: