CSS and Sass
CSS/Sass
Philosophy
We value content and the experience users will have reading it. We write CSS with this in mind and don't sacrifice our clients' most important assets over the latest, shiniest, half-supported CSS features just for the sake of using them. CSS should help enhance content, not bury it under "cool" distractions.
Our websites are built mobile first, using performant CSS. Well-structured CSS yields maintainability and better collaboration which ultimately yields better client experiences.
Accessibility
Animation
Not every animation brings pleasure to the end user. In some cases motion can trigger harmful reactions from users with vestibular disorders, epilepsy or even migraines.
The prefer-reduced-motion CSS media feature is recommended for use when working with animation, as it's simple to implement and affords a better experience for those who need it.
Here is an example:
.animation {
animation: vibrate 0.3s linear infinite both;
}
@media (prefers-reduced-motion: reduce) {
.animation {
animation: none;
}
}
Read more about creating accessible animations.
Performance
Let's be honest, CSS "speed" and performance is not as important as back end or JavaScript performance. However, this doesn't mean we should ignore it. A sum of small improvements equals better experience for the user.
Three areas of concern are network requests, CSS specificity, and animation performance.
Performance best practices are not only for the browser experience, but for code maintenance as well.
Responsive Design
We build our websites mobile first. We leverage a natural, mobile-first build process and allow sites gracefully degrade. We have found this to be the best way to maintain code quality and utilize more forward-thinking CSS.
Min-width media queries
A responsive website should be built with min-width media queries. This approach means that our media queries are consistent, readable and minimize selector overrides.
- For most selectors, properties will be added at later breakpoints. This way we can reduce the usage of overrides and resets.
- It targets the lowest common denominator first, which is philosophically in line with mobile first — a concept we embrace for our sites.
- When media queries consistently "point" in the same direction, it makes it easier to understand and maintain stylesheets.
Avoid mixing min-width and max-width media queries if possible, except in situations where notably less code would need to be written.
Breakpoints
Working with build tools that utilize Sass or PostCSS processing, we can take advantages of reusability and avoid having an unmaintainable number of breakpoints. Using variables and reusable code blocks we can lighten the CSS load and ease maintainability. We maintain a flexible Sass library for breakpoints that you can use in new projects.
Media queries placement
In your stylesheet files, nest the media query within the component it modifies. Do not create size-based partials (e.g. _1024px.(s)css, _480px.(s)css): it will be frustrating to hunt for a specific selector through all the files when we have to maintain the project. Putting the media query inside the component will allow developers to immediately see all the different styles applied to an element.
Avoid:
@media only screen and (min-width: 1024px) {
@import "responsive/1024up";
}
.some-class {
color: red;
}
.some-other-class {
color: orange;
}
@media only screen and (min-width: 1024px) {
.some-class {
color: blue;
}
}
Prefer:
.some-class {
color: red;
@media only screen and (min-width: 1024px) {
color: blue;
}
}
.some-other-class {
color: orange;
}
CSS Syntax
Syntax and formatting are keys to a maintainable project. By keeping our code style consistent, we not only help ourselves debug faster but we're also lessening the burden on those who will have to maintain our code (maybe ourselves too!).
CSS syntax is not strict and will accept a lot of variations, but for the sake of legibility and fast debugging, we follow basic code styles:
- Write one selector per line
- Write one declaration per line
- Closing braces should be on a new line
Avoid:
.class-1, .class-2,
.class-3 {
width: 10px; height: 20px;
color: red; background-color: blue; }
Prefer:
.class-1,
.class-2,
.class-3 {
width: 10px;
height: 20px;
color: red;
background-color: blue;
}
- Include one space before the opening brace
- Include one space before the value
- Include one space after each comma-separated values
Avoid:
.class-1,.class-2{
width:10px;
box-shadow:0 1px 5px #000,1px 2px 5px #ccc;
}
Prefer:
.class-1,
.class-2 {
width: 10px;
box-shadow: 0 1px 5px #000, 1px 2px 5px #ccc;
}
- Try to use lowercase for all values, except for font names
- Zero values don't need units
- End all declarations with a semi-colon, even the last one, to avoid errors
- Use double quotes for selectors, single quotes for properties
Avoid:
[data-foo='bar'] {
background-color: #FFFFFF;
background-image: url(https://test.com/image.jpg);
font-family: Helvetica Neue, sans-serif;
margin: 0px
}
Prefer:
[data-foo="bar"] {
background-color: #fff;
background-image: url('https://test.com/image.jpg');
font-family: 'Helvetica Neue', sans-seriff;
margin: 0;
}
If you don't need to set all the values, don't use shorthand notation.
Avoid:
.header-background {
background: blue;
margin: 0 0 0 10px;
}
Prefer:
.header-background {
background-color: blue;
margin-left: 10px;
}
If you have many similar selectors with a small amount of properties grouped together, it's okay to put them on one line for better readability, but often those can be created programmatically in a Sass for loop.
.column-1 { width: 8.3333%; }
.column-2 { width: 16.6667%; }
.column-3 { width: 25%; }
.column-4 { width: 33.3333%; }
Declaration Ordering
Declarations should be ordered alphabetically or by type (positioning, box model, typography, visual). Whichever order is chosen, it should be consistent across all files in the project.
If you're using Sass, use this ordering:
@extend(although it should be used sparingly)- Regular styles (allows overriding extended styles)
@include(to visually separate mixins and placeholders) and media queries- Nested selectors
Nesting
Nesting your code is useful to a point, but too much nesting can be difficult to follow, and also adds unnecessary specificity, forcing us to add the same or greater specificity in overrides. We want to avoid selector nesting and over-specificity as much as possible.
If you're using PostCSS or Sass, nesting should be done in the following cases, because it will make the code easier to read:
- Pseudo-classes
- Pseudo-elements
- Component states
- Media queries
Avoid nesting more than two levels when using BEM syntax. Nesting more deeply generally indicates that the naming convention used wasn't robust enough or is not being used appropriately.
Avoid:
.car__door {
.car__door--passenger-side { /* ... */ }
}
// This makes car__door--passenger-side harder to search for in a file
.car__door {
&--passenger-side { /* ... */ }
}
Prefer:
.car__door { /* ... */ }
.car__door--passenger-side { /* ... */ }
Selector Naming
We use the BEM naming convention as a general guide for class naming. BEM stands for Block-Element-Modifier, and follows a certain style of delineation to denote which level of item that we are modifying.
Why BEM? According to Robin Rendle in his BEM 101 CSS-Tricks article, BEM is used because:
- If we want to make a new style of a component, we can easily see which modifiers and children already exist. We might even realize we don’t need to write any CSS in the first place because there is a pre-existing modifier that does what we need.
- If we are reading the markup instead of CSS, we should be able to quickly get an idea of which element depends on another [...]
- Designers and developers can consistently name components for easier communication between team members. In other words, BEM gives everyone on a project a declarative syntax that they can share so that they're on the same page.
An element is delineated by underscores __, and a modifier by dashes --. All elements of the BEM class should be lowercased.
.block__element--modifier { }
This would carry out in the following example:
.car { /* ... */ }
.car__door { /* ... */ }
.car--blue { /* ... */ }
Our block is a car – this represents the whole of what we're styling. Then, we have an element of __door – this represents a single piece of the larger block that we want to style. Finally, we have a modifier of --blue to describe a variation of the car. Perhaps our car was originally red, but we need to style this one as blue.
You can read more on BEM at the Get BEM website.
For components that could possibly conflict with plugins or third-party libraries, use vendor prefixes. Don’t use names that can be blocked by adblockers (e.g. "advertisement"). When in doubt, you can check a class name against this list to see if it's likely to be blocked.
Components
Sass/SCSS should be written in a highly componentized manner. Each component should have its own file and specific set of styles that relate to it and it alone. These styles can be extended, but generally should be very specific for each use case. This allows us to use a singular set of markup and styles all across a site with predictable results.
As mentioned in the block section, sub-components are indicated with an underscore, variants with a hyphen. For example:
.some-component
.some-component__sub-component
.some-component--variation
Components should be placed in a file named for the component, with an underscore in front to denote it as a partial: _some-component.scss. When you include _ in front of the filename, it won't be generated as CSS unless you import it into another Sass file that is not partial.
State
If a component has state (often determined by JavaScript) then you should set a class on the component to indicate the current state.
You should use the BEM modifier syntax, on a per-component basis, with grammatically correct modifier names to indicate the state. For example .some-component--is-open, .some-component--has-option-selected.
You should avoid using global classes such as .is-visible (refer to the BEM FAQ for more info).
You should always use the modifier class on the top level block/component, never on the child element.
Avoid:
// No global modifiers
<div class="some-component is-active">
<div class="some-component__heading"></div>
<div class="some-component__content"></div>
</div>
// Set the state on the on the top level component/block and not on a child element
<div class="some-component">
<div class="some-component__heading"></div>
<div class="some-component__content some-component__content--is-visible"></div>
</div>
Prefer:
<div class="some-component some-component--is-active">
<div class="some-component__heading"></div>
<div class="some-component__content"></div>
</div>
JavaScript Hooks
JavaScript hooks should be kept separate from those used for styles. This allows developers to move the JavaScript behavior without the need to modify the CSS. Our preference is for JavaScript hooks to make use of data attributes, e.g. [data-lightbox]. If a JavaScript component must use a class, it should remain separate from styles and be prefixed with js-.
Documentation
Code documentation serves two purposes: it makes maintenance easier and it makes us stop and think about our code. If the explanation is too complex, maybe the code is overly complex too. Documenting helps keep our code simple and maintainable.
Commenting
While we feel that CSS is generally self-documenting through a browser inspector, do not hesitate to be verbose with your comments, especially when documenting a tricky part of your CSS. Use comment blocks to separate the different sections of a partial, and/or to describe what styles the partial covers:
/**
* Section title
*
* Description of section
*/
For single selectors or inline comments, use this syntax:
/* Inline comment */
// Inline comment for SCSS
Make sure to comment any complex selector or rule you write. For example:
// Select list item 4 to 8, included
li:nth-child(n+4):nth-child(-n+8) {
color: red;
}
Best Practices
Network Requests
- Limit the number of requests by concatenating CSS files and encoding assets to the CSS file where appropriate.
- Minify stylesheets
- Split CSS files where you have view-specific styling, like an account page.
Automate these tasks with a build process.
CSS Specificity
Specificity across styles is important to the long-term maintainability. Over-specifying at any point on the stylesheet makes it more difficult to make changes or adjustments in the future.
For this reason, the preferred identifier for rules is to use a highly-distinct class name. Classes are distinct without using a high specificity index rating and are re-usable where needed. Using the BEM onvention mentioned previously will help avoid the need to use IDs, !important, or over-nest your rules.
Avoid using !important whenever you can. One acceptable use case would be overriding uncontrolled third-party code, but there is almost always a better solution outside of that.
Use efficient selectors.
Avoid:
div div header#header div ul.nav-menu li a.black-background {
background: radial-gradient(ellipse at center, #a90329 0%,#8f0222 44%,#6d0019 100%);
}
Prefer:
.nav-menu__link {
background: radial-gradient(ellipse at center, #a90329 0%,#8f0222 44%,#6d0019 100%);
}
General tag identifiers p, a, li, h1, etc. should be avoided unless the style is a global one. For example, using a general tag when styling all <h1> tags on the site is okay, styling a specific page's <h1> using a tag is not.
Avoid:
#cards { /* ... */ }
.card { /* ... */ }
.card ul { /* ... */ }
.card ul li p a { /* ... */ }
.single-post h1 { /* ... */ }
Prefer:
.card-list { /* ... */ }
.card-single { /* ... */ }
.card-single__link { /* ... */ }
.single-post__page-title { /* ... */ }
Inheritance
Fortunately, many CSS properties can be inherited from the parent. Take advantage of inheritance to avoid bloating your stylesheet but keep specificity in mind.
Avoid:
.sibling-1 {
font-family: Arial, sans-serif;
}
.sibling-2 {
font-family: Arial, sans-serif;
}
Prefer:
.parent {
font-family: Arial, sans-serif;
}
Reusable code
Styles that can be shared, should be shared (aka DRY, Don't Repeat Yourself). This will make our stylesheets less bloated and prevent the browser from doing the same calculations several times. Make good use of Sass placeholders.
CSS over assets
Don't add an extra asset if a design element can be translated in the browser using CSS only. We value graceful degradation over additional HTTP requests.
Very common examples include gradients and triangles.
Animations
It's a common belief that CSS animations are more performant than JavaScript animations. This is not always the case.
- If you're only animating simple state changes and need good mobile support, go for CSS (most cases).
- If you need more complex animations, use a JavaScript animation framework or requestAnimationFrame.
Limit your CSS animations to just the properties you're animating, and prefer the use hardware-accelerated animations like 3D transforms (translate, rotate, scale) and opacity. Note that too much reliance on the GPU can also overload it.
Avoid:
#menu li {
transition: all 1s ease-in-out;
}
#menu li:hover {
opacity: 0.8;
}
Prefer:
#menu li {
transition: transform 1s ease-in-out;
}
#menu li:hover {ß
opacity: 0.8;
}
Always test animations on a real mobile device loading real assets, to ensure the limited memory environment doesn't tank the site. Note: WCAG 2.1, Guideline 2.3.2 Motion from Animation dictates that, "Motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed."
Set Variables
Defining Sass variables and mixins, as well as CSS custom properties, should be part of the initial setup, and can make the project much more maintainable.
Here are some common properties that will benefit from variables:
border-radiuscolorfont-familyfont-weightmargin (gutters, grid gutters)transition (duration, easing)– consider a mixin
Avoid Magic Numbers
Try not to set arbitrary numbers because they "just work." other developers might not understand why the property has to be set in such a particular way. Instead, create relative values whenever possible.
Avoid:
.logo {
left: 20px;
}
Prefer:
.logo {
left: ($gutter - ($nav-height / 2));
}
//or
.logo {
left: calc(var(--gutter) - (var(--nav-height) / 2));
}
See much more on magic numbers from CSS-Tricks.
Be Descriptive
It's easy to define CSS selectors according to their looks; it's better to describe their hierarchy in your code.
Avoid:
.huge-font {
font-family: 'Impact', sans-serif;
}
.blue {
color: $color-blue;
}
Prefer:
.brand__title {
font-family: 'Impact', serif;
}
// Utility prefix u-
.u-highlight {
color: $color-blue;
}
Frameworks
Grids
For CRO work and enhancing existing themes, our preference is not to use a third-party grid system, use your best judgement in working with what you have, and keep it simple. For larger scale and new projects, a more complex grid system may be warranted, and leveraging a third-party library will gain some efficiency.
When working with design comps, remember that all too often we are faced with a layout that isn't built on a grid or purposefully breaks a loosely defined grid. Even if the designer had a grid in mind, there are often needs that require more creative solutions. For example: fixed-width content areas to accommodate advertising. Don't be afraid to break out of the grid as needed.
Resets
Normalize.css is our primary tool for CSS resets.
Structure
Well thought-out folder structure helps with maintaining your project. The same goes for your styles. It's important to divide your code according to the elements they refer to. Storing all your rules in one huge styles.scss file would lead to issues as you scale up your project and add new components.
Below is a simple example folder structure you might use:
├── /base
│ ├── _core.scss
│ ├── _colors.scss
│ ├── _settings.scss
│ └── _typography.scss
├── /utils
│ ├── _animations.scss
│ ├── _breakpoints.scss
│ ├── _grid.scss
│ └── _misc.scss
├── /components
│ ├── _header.scss
│ ├── _footer.scss
│ ├── _button.scss
│ └── _sidebar.scss
├── /vendor
│ └── normalize.css
└── main.scss
And your main.scss file may look like this:
// *** Vendor ***
@import './vendor/normalize.css';
// *** Settings ***
@import './base/settings';
@import './base/colors';
// *** Utils ***
@import './utils/easings';
@import './utils/animations';
@import './utils/grid';
@import './utils/ui';
// *** Base ***
@import './base/core';
@import './base/typography';
// *** Components ***
@import './components/header';
@import './components/footer';
@import './components/button';
@import './components/sidebar';
On even larger projects, you may want to utilize the 7-1 pattern or something similar. There are no set guidelines here; just make sure that your structure is easy to follow and breaks styles up effectively.