The ten commandments of sane style sheets
The Ten Commandments
- Thou shalt have a single source of truth for all key selectors
- Thou shalt not nest (unless thou art nesting media queries, overrides or thou really hast to)
- Thou shalt not use ID selectors (even if thou thinkest thou hast to)
- Thou shalt not write vendor prefixes in the authoring style sheets
- Thou shalt use variables for sizing, colours and z-index
- Thou shalt always write rules mobile first (avoid max-width)
- Thou shalt use mixins for font-stacks
- Thou shalt comment all magic numbers and browser hacks
- Thou shalt not inline images
- Thou shalt not write complicated CSS when simple CSS will work just as well
Blessed are those that follow these rules for they shall inherit sane style sheets.
Amen.
Why the ten commandments?
CSS is global in nature. It’s currently impossible to scope rules to particular areas of a UI. Therefore any sizeable CSS codebase can become difficult to maintain. The global nature of CSS means styles can leak from the intended target to other elements. In addition, with large CSS files, the cascade of styles begins to create undesirable effects, as rules in one location of the CSS can effect (or not) other rules.
CSS can also be authored in a way that relies upon the structure of the DOM. This too is problematic and inflexible. Descendant selectors, ID based selectors and overly specific selectors all contribute towards CSS that is hard to maintain and reason about and brittle in terms of DOM changes.
We can attempt to address this issue in two ways. Firstly, the strict adherence to a CSS class based naming convention. This is documented in detail elsewhere. The other way is by following some rules that contribute towards more maintainable and enduring style sheets.
Tooling
To achieve more maintainable style sheets we can lean upon PostCSS, a piece of CSS tooling that allows the manipulation of CSS with JavaScript. The curious can look here for more information: https://github.com/postcss/postcss
PostCSS facilitates the use of an extended CSS syntax. This provides functionality to make our authoring style sheets easier to maintain.
Using PostCSS we are able to make use of:
- variables
- mixins (like macros for certain settings such as font-families)
- referencing a key-selector with an ampersand symbol (
&
) - the ability to write loops for lengthy rule sets (100 variants of different coloured headers for example)
Practically, PostCSS is similar in functionality to a CSS pre-processor such as Sass, LESS or Stylus.
Where it differs is in its modularity and extensibility. Rather than ‘swallow the whole pill’ as is needed with the aforementioned pre-processors, using PostCSS allows us to be more selective about the abstractions we employ and how we analyse and maintain those abstractions from an authoring point of view . It also gives us the ability to extend our CSS abstractions should we wish to.
In addition it allows us to perform static analysis of the authoring styles via linting and can fail builds when undesirable code is authored.
Rationale
We want to avoid producing CSS that suffers from being overly specific, littered with unneeded prefixes, poorly commented and full of ‘magic’ numbers.
The following 10 rules set-out what are considered to be the most important rules to achieve this goal.
Definitions used throughout:
- override: a situation where the values of a key selector are purposely amended based upon inheritance
- key selector: the right most selector in any CSS rule
- prefixes: vendor specific prefixes e.g. -webkit-transform:
- authoring style sheets: the files we author the rules in
- CSS: the resultant CSS file generated by the tooling and ultimately consumed by the browser
Expanded Information on the Ten Commandments
There is rationale behind each of the 10 commandments. We will look at of these now.
1. Thou shalt have a single source of truth for all key selectors
In the authoring style sheets, a key selector should only be written once.
This allows us to search for a key-selector in the code base and find a ‘single source of truth’ for our selector. Thanks to the use of an extended CSS syntax, EVERYTHING that happens to that key selector can be encapsulated in a single rule block.
Overrides to the key selector are handled by nesting and referencing the key selector with the ‘parent’ selector. More of which shortly.
Consider this example:
.key-Selector {
width: 100%;
@media (min-width: $M) {
width: 50%;
}
.an-Override_Selector & {
color: $color-grey-33;
}
}
That would yield the following in the CSS:
.key-Selector {
width: 100%;
}
@media (min-width: 768px) {
.key-Selector {
width: 50%;
}
}
.an-Override_Selector .key-Selector {
color: #333;
}
In the authoring style sheets, the key selector (.key-Selector
) is never repeated at a root level. Therefore, from a maintenance point of view, we only have to search for .key-Selector
in the code base and we will find everything that could happen to that key selector described in a single location; a single source of truth.
- What happens if we need it to display differently in a different viewport size?
- What happens when it lives within containerX?
- What happens when this or that class gets added to it via JavaScript?
In all these instances the eventualities for that key selector are nested within that single rule block. Let’s look at overrides in further detail next.
Dealing with overrides
In the prior example, there was a demonstration of how to deal with an override to a key selector. We nest the overriding selector inside the rule block of the key selector and reference the parent with the &
symbol. The &
symbol, as in the Sass language, is a parent selector. You can think of it like this.
in JavaScript.
Standard override
Consider this example:
.ip-Carousel {
font-size: $text13;
/* The override is here for when this key-selector sits within a ip-HomeCallouts element */
.ip-HomeCallouts & {
font-size: $text15;
}
}
This would yield the following CSS:
.ip-Carousel {
font-size: 13px;
}
.ip-HomeCallouts .ip-Carousel {
font-size: 15px;
}
This results in a font-size increase for the ip-Carousel
when it is inside an element with a class of ip-HomeCallouts
.
override with additional class on same element
Let’s consider another example, what if we need to provide an override when this element gets an additional class? We should do that like this:
.ip-Carousel {
background-color: $color-green;
&.ip-ClassificationHeader {
background-color: $color-grey-a7;
}
}
That would yield this CSS:
.ip-Carousel {
background-color: #14805e;
}
.ip-Carousel.ip-ClassificationHeader {
background-color: #a7a7a7;
}
Again, the override is contained within the rule block for the key selector.
override when inside another class and also has an additional class
Finally let’s consider how to do contain the eventuality where we need to provide an override where the key selector is inside another element and also has an additional class present:
.ip-Carousel {
background-color: $color-green;
.home-Container &.ip-ClassificationHeader {
background-color: $color-grey-a7;
}
}
That would yield the following CSS:
.ip-Carousel {
background-color: #14805e;
}
.home-Container .ip-Carousel.ip-ClassificationHeader {
background-color: #a7a7a7;
}
We have used the parent selector here to reference our key selector between an override above (.home-Container
) and alongside another class (.ip-ClassificationHeader
).
override with media queries
Finally, let’s consider overrides with media queries. Consider this example:
.key-Selector {
width: 100%;
@media (min-width: $M) {
width: 50%;
}
}
That would yield this CSS:
.key-Selector {
width: 100%;
}
@media (min-width: 768px) {
.key-Selector {
width: 50%;
}
}
Again, all eventualities contained within the same rule. Note the use of a variable for the media query width. We will come on to that shortly.
Any and all media queries should be contained in the same manner. Here’s a more complex example:
.key-Selector {
width: 100%;
@media (min-width: $M) and (max-width: $XM) and (orientation: portrait) {
width: 50%;
}
@media (min-width: $L) {
width: 75%;
}
}
That would yield this CSS:
.key-Selector {
width: 100%;
}
@media (min-width: 768px) and (max-width: 950px) and (orientation: portrait) {
.key-Selector {
width: 50%;
}
}
@media (min-width: 1200px) {
.key-Selector {
width: 75%;
}
}
With all the nesting of overrides we have just looked at, you may think it makes sense to nest child elements too? You are wrong. Very wrong. This would be a very very bad thing to do. We’ll look at why next.
2. Thou shalt not nest (unless thou art nesting media queries, overrides or thou really hast to)
The key selector in CSS is the rightmost selector in any rule. It is the selector upon which the enclosed property/values are applied.
We want our CSS rules to be as ‘flat’ as possible. We DO NOT want the other selectors before a key selector (or any DOM element) unless we absolutely need them to override the default key selector styles. Adding additional selectors and using element types (for example h1.yes-This_Selector
):
- creates additional unwanted specificity
- makes it harder to maintain as overrides need to be ever more specific
- adds unneeded bloat to the resultant CSS file
- in the case of element types, ties the rule to a specific element
For example, suppose we have a CSS rule like this:
#notMe .or-me [data-thing="nope"] .yes-This_Selector {
width: 100%;
}
In that above example, yes-This_Selector
is the key selector. If those property/values should be added to the key selector in all eventualities, we should make a simpler rule.
To simplify that prior example, if all we want to target is the key-selector we would want a rule like this:
.yes-This_Selector {
width: 100%;
}
Don’t nest children within a rule
Suppose we have a situation where we have a video play button inside a wrapping element. Consider this markup:
<div class="med-Video">
<div class="med-Video_Play">Play</div>
</div>
Let’s set some basic styling for the wrapper:
.med-Video {
position: absolute;
background-color: $color-black;
}
Now we want to position the play element within that wrapping element. You might be tempted to do this:
WARNING: EXAMPLE OF INCORRECT CODE
.med-Video {
position: absolute;
background-color: $color-black;
/* Center the play button */
.med-Video_Play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
That would yield this CSS (vendor prefixes removed for brevity):
.med-Video {
position: absolute;
background-color: #000;
/* Center the play button */
}
.med-Video .med-Video_Play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Do you see the problem here? We have introduced additional specificity for our .med-Video_Play
element when it is completely unneeded.
This is a very subtle illustration. However, it is important to be aware of, and avoid, doing this as we don’t want rules like this:
WARNING: EXAMPLE OF INCORRECT CODE
.MarketGrid > .PhoneOnlyContainer > .ClickToCallHeader > .ClickToCallHeaderMessage > .MessageHolder > span {
font-weight: bold;
padding-right: 5px;
}
Instead, remember that each key selector gets its own rule block. Overrides are nested, child elements are not. Here is that example rewritten correctly:
.med-Video {
position: absolute;
background-color: $color-black;
}
/* Center the play button */
.med-Video_Play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
That would yield this CSS:
.med-Video {
position: absolute;
background-color: #000;
}
/* Center the play button */
.med-Video_Play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Each key selector is only as specific as it needs to be and no more.
3. Thou shalt not use ID selectors (even if thou thinkest thou hast to)
The limitations of IDs in a complex UI are well documented. In summary, they are far more specific than a class selector – therefore making overrides more difficult and they can only be used once in the page so their efficacy is limited.
Tip: for more on specificity, http://www.w3.org/TR/CSS21/cascade.html#specificity
Do not use ID selectors in the CSS. They present no advantages over class based selectors and introduce unwanted problems.
In the almost unbelievable situation where you HAVE to use an ID to select an element, use it within an attribute selector instead to keep specificity lower:
[id="Thing"] {
/* Property/Values Here */
}
4. Thou shalt not write vendor prefixes in the authoring style sheets
Thanks to PostCSS, we now have tooling that means it is unnecessary to write vendor prefixes for any W3C specified property/values in the authoring style sheets. The prefixes are handled auto-magically by the Autoprefixer tool that can be configured to provide vendor prefixes for the required level of platforms/browser support.
For example, don’t do this:
WARNING: EXAMPLE OF INCORRECT CODE
.ip-Header_Count {
position: absolute;
right: $size-full;
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
Instead you should just write this:
.ip-Header_Count {
position: absolute;
right: $size-full;
top: 50%;
transform: translateY(-50%);
}
Not only does this make the authoring style sheets easier to read and work with, it also means that when we want to change our level of support we can make a single change to the build tool and the vendor prefixes that get added will update automatically.
The only exception to this scenario is non-W3C property/values that might still be desirable. For example, for touch inertia scrolling panels in WebKit devices, it will still be necessary to add certain vendor prefixed properties in the authoring styles as they are non-W3C. For example:
.ui-ScrollPanel {
-webkit-overflow-scrolling: touch;
}
Or in the case of removing the scrollbar for WebKit:
.ui-Component {
&::-webkit-scrollbar {
-webkit-appearance: none;
}
}
5. Thou shalt use variables for sizing, colours and z-index
For any project of size, setting variables for sizing, colours and z-index is essential.
UIs are typically based upon some form of grid or sizing ratio. Therefore sizing should be based upon set sizes, and sensible delineations of those sizes. For example here is 11px based sizing and variants as variables:
$size-full: 11px;
$size-half: 5.5px;
$size-quarter: 2.75px;
$size-double: 22px;
$size-treble: 33px;
$size-quadruple: 44px;
For a developer, the use of variables offers additional economies. For example, it saves colour picking values from composites. It also helps to normalise designs.
For example, if a project uses only 13, 15 and 22px font sizes and a change comes through requesting 14px font-sizing, the variables provide some normalisation reference. In this case, should the fonts be 13 or 15px as 14 is not used anywhere else? This allows developers to feedback possible design inconsistencies to the designers.
The same is true of colour values. For example, suppose we have a variable for the hex #333
. We can write that as a variable like this:
$color-grey-33: #333;
On the surface it seems ridiculous to write the variable name when the hex value is shorter. However, again, using variables prevents unwanted variants creeping in to the code base (e.g. #323232
) and helps identify ‘red flags’ in the code.
It’s also important to still use the variables when making amendments to colours. Use colour functions on the variables to achieve your goal. For example, suppose we want a semi-opaque #333
colour.
That should be achieved in the authoring style sheets like this:
.ip-Header {
background-color: color($color-grey-33 a(.5));
}
PostCSS can provide a polyfill for the W3C colour functions: https://drafts.csswg.org/css-color/#modifying-colors
and the example above yields this CSS:
.ip-Header {
background-color: rgba(51, 51, 51, 0.5);
}
In this example we have used the alpha CSS colour function. We use the color()
function, pass in the colour we want to manipulate and then the manipulation (alpha in this instance).
Using the variables can initially seem more complex but makes it easier for future authors to reason about what colour is being manipulated.
The use of variables for z-index is equally important. This enforces some sanity when it comes to stacking contexts. There should be no need for z-index: 999
or similar. Instead, use one of only a few defaults (set as variables). Here are relevant variables for z-index:
$zi-highest: 50;
$zi-high: 40;
$zi-medium: 30;
$zi-low: 20;
$zi-lowest: 10;
$zi-ground: 0;
$zi-below-ground: -1;
6. Thou shalt always write rules mobile first (avoid max-width)
For any responsive work, we want to embrace a mobile-first mentality in our styles. Therefore, the properties and values within the root of a rule should be the properties that apply to the smallest viewports (e.g. mobile). We then use media queries to override or add to these styles as and when needed.
Consider this:
.med-Video {
position: relative;
background-color: $color-black;
font-size: $text13;
line-height: $text15;
/* At medium sizes we want to bump the text up */
@media (min-width: $M) {
font-size: $text15;
line-height: $text18;
}
/* Text and line height changes again at larger viewports */
@media (min-width: $L) {
font-size: $text18;
line-height: 1;
}
}
That would yield this CSS:
.med-Video {
position: relative;
background-color: #000;
font-size: 13px;
line-height: 15px;
}
@media (min-width: 768px) {
.med-Video {
font-size: 15px;
line-height: 18px;
}
}
@media (min-width: 1200px) {
.med-Video {
font-size: 18px;
line-height: 1;
}
}
We only need to change the font-size and line-height at different viewports so that is all we are amending. By using min-width (and not max-width) in our media query, should the font-size and line-height need to stay the same at a larger size viewport we wouldn’t need any extra media queries. We only need a media query when things change going up the viewport size range. To this ends, the use of max-width as the single argument of a media query is discouraged.
Bottom line: write media queries with min-width not max-width. The only exception here is if you want to isolate some style to a middle range. For example between medium and large viewports. Example:
.med-Video {
position: relative;
background-color: $color-black;
font-size: $text13;
line-height: $text15;
/* Between medium and large sizes we want to bump the text up */
@media (min-width: $M) and (max-width: $L) {
font-size: $text15;
line-height: $text18;
}
}
7. Thou shalt use mixins for font-stacks
Font stacks are difficult to get right and tedious to author. The sanest way to deal with fonts is to have the body
use the most common font stack and then only override this with a different font-stack as and when needed.
For example:
.med-Video {
position: relative;
background-color: $color-black;
font-size: $text13;
line-height: $text15;
/* At medium sizes we want to bump the text up */
@media (min-width: $M) {
@mixin FontHeadline;
font-size: $text15;
line-height: $text18;
}
}
For simpler font-stacks a variable can handle this need easily so may be preferable. Mixins are used for more complex stacks, where it’s preferable to have certain font stacks apply in different situations. For example, perhaps even for the body text, one font is required for LoDPI, and another for HiDPI. These situations can’t be dealt with by using a variable alone so a mixin is called as needed.
8. Thou shalt comment all magic numbers and browser hacks
The variables.css file should contain all variables relevant to the project. If a situation arises where a pixel based value needs entering into the authoring style sheets that isn’t already defined in the variables.css this should serve as a red flag to you. This scenario is also covered above. In the case where a ‘magic’ number needs entering in the authoring style sheets, ensure a comment is added on the line above to explain it’s relevance. This may seem superflous at the time but think of others and yourself in 3 months time. Why did you add a negative margin of 17 pixels to that element?
Example:
.med-Video {
position: relative;
background-color: $color-black;
font-size: $text13;
line-height: $text15;
/*We need some space above to accommodate the absolutely positioned icon*/
margin-top: 20px;
}
The same goes for any device/browser hacks. You may have your syntax but I use a comment above the start of the hack code with the prefix /*HHHack:*/
when I have to add code purely to satisfy a particular situation. Consider this:
.med-Video {
background-color: $color-black;
font-size: $text13;
line-height: $text15;
/*HHHack needed to force Windows Phone 8.1 to render the full width, reference ticket SMP-XXX */
width: 100%;
}
These kinds of overrides should be bottom-most in the rule if at all possible. However, make sure you add a comment. Otherwise, future authors make look at your code and presume the line(s) are superfluous and remove them.
9. Thou shalt not place inline images in the authoring style sheets
While we continue to support HTTP based users (as opposed to HTTP2) the practice of inlining assets provides some advantages; primarily it reduces the number of HTTP requests required to serve the page to the user. However, placing inline assets in the authoring style sheets is discouraged.
Consider this:
.rr-Outfit {
min-height: $size-quadruple;
background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAA3NCSVQICAjb4U/gAAAAw1BMVEX///8AAACEhIQAAABYWFgAAAAAAAAAAAB6enomJiYAAAAiIiJ4eHgAAABiYmJgYGAAAACJiYkAAAC0tLR/f3/IyMjHx8e/v7+vr6+fn5+ZmZmUlJSBgYFmZmYiIiIQEBAAAAD////w8PDv7+/i4uLh4eHf39/W1tbS0tLR0dHMzMzHx8fFxcXCwsK/v7+4uLi0tLSvr6+rq6ulpaWfn5+ZmZmQkJCPj498fHxwcHBgYGBAQEAwMDAgICAfHx8QEBAAAAAphWZmAAAAQXRSTlMAESIiMzNEVWZmZneIiKqqu8zM3d3u7u7u7u7u7u7u7u7//////////////////////////////////////////wP/q/8AAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAVdEVYdENyZWF0aW9uIFRpbWUAMjEvNS8xMpVX8IQAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAAAw0lEQVQYlU2P6VLCUAyFD1KvWFQ2F1ChnLLW2oIs1ZZC8/5PRW4rM+RH5uSbLCeARt2YO2Pq+I+aafUjRs+PplbVzfeQcbyZzoZuSZoBdyKSkQxdOx+SuQKZK/m8BZ7Iia3lT8m6BQy4/NodpYjpb9OiC3i/U+1NfE08iAdIamXACggwzq7BCGgfrIxt8pOsDbjpRu/k+Q/DBWf3auSVq/1Rz55074d16gT0i8pq4JTPOC9MRIqEbzeXfx860eS71yj1GWENHluGjvJwAAAAAElFTkSuQmCC);
}
How is a future author supposed to reason about what that asset is?
Instead, let the tooling inline the image for you. This means the authoring style sheets can provide a clue as to what the image might be but also enables that image to be more easily swapped out. You can inline images with the inline
command. Here’s that prior example re-written correctly:
.rr-Outfit {
min-height: $size-quadruple;
background-image: inline("/path/to-image/relevant-image-name.png");
}
10. Thou shalt not write complicated CSS when simple CSS will work just as well
Try and write CSS code that is as simple as possible for others to reason about in future. Writing loops, mixins and functions should be minimised or used very seldom. As a general rule, if there are less than 10 variations of a rule, write it ‘by-hand’. If on the other hand you need to create background positions for a sprite sheets of 30 images, this is something that tooling should be used for.
The creation and use of mixins is discouraged. The overuse of mixins can create code that is overly abstracted from the result. While mixins should be used for font-stacks or to automatically yield error prone strings there are few additional situations where they prove worthwhile.
This pursuit of simplicity should be extended in the manner layouts are achieved. If a better supported layout mechanism achieves the same goal with the same amount of DOM nodes as a less well supported one, use the former. However, if a different layout mechanism reduces the number of DOM nodes needed or presents additional benefits yet is simply hard to understand or alien (I’m thinking of you Flexbox), take the time to understand the benefits it might offer.
Enforcement of rules with tooling
Rules are nothing without enforcement. Where possible, static analysis of authoring style sheets can provide real-time feedback to ‘reject’ the writing of CSS code that obviously runs counter to these rules.
At the time of writing, Stylelint is used as part of the PostCSS tooling to provide feedback on authoring styles as written.
Out of the box, stylelint can provide feedback on the command line (and should be configured to fail builds for non-compliant code) but plugins exist for Sublime Text and Atom to provide realtime feedback of potential problems (a .stylelintrc
file in the project repository can provide team-agreed rules that should be enforced).
“unless thou nests” → “unless thou art nesting”