CSS best practices
Syntax and formatting
Tab indentation
Use tabs rather than spaces for indentation, with a default tab width equivalent to four (4) spaces.
Developers can customise the tab space within their own environment when working with code, to make it easier for them to read, but should re-indent to the default before committing their work.
120 character line length
Where possible, limit line length to 120 characters, to make it easier to have multiple files open side-by-side if needed and to provide a comfortable line length for comments. Ignore unavoidable exceptions, such as URLs or gradient syntax.
Title every new major section of CSS
When working on a project where each section has its own file, the title should appear at the top of each file. If there are multiple sections per file, precede each title with five (5) empty lines. This makes it easier to spot new sections when scrolling through large files.
Prefix the title with a hash (#
) symbol to allow for more targeted searches. Leave a carriage return between the title and the next line of code.
/*------------------------------------*\ #A section \*------------------------------------*/ .selector {} /*------------------------------------*\ #Another section \*------------------------------------*/ /** Comment */ .another-selector {}
The anatomy of a CSS ruleset
[selector] { [property]: [value]; [<--declaration--->] } /** For example: */ th, td, table caption { padding: 0.75rem; text-align: left; }
The preceding example shows:
- selectors comma-separated onto new lines
- the opening curly brace (
{
) on the same line as the last selector - a space before the opening curly brace
- the first declaration on a new line after the opening curly brace
- properties and values on the same line
- a space after the property-value delimiting colon (
:
) - each declaration on its own line
- each declaration ended with a semicolon (
;
) - each declaration tab-indented
- closing curly brace (
}
) on its own new line
Declaration order
Organise declarations in ascending alphabetical order for consistent code that is easier to learn, remember and manually maintain.
If manually adding vendor prefixes, ignore these for sorting purposes. However, multiple vendor-specific prefixes for a certain CSS property should remain alphabetically sorted (e.g. -moz
comes before -webkit
).
Multi-line CSS
Write CSS across multiple lines, except in very specific circumstances. Doing so:
- reduces the chance of merge conflicts, because each piece of functionality exists on its own line
- results in more reliable diffs, because one line only ever carries one change
Exceptions to this rule should be self-evident, such as similar rulesets that only carry one declaration each, for example:
.note { border-width: 5px; } .note--error {border-color: #b83123;} .note--info {border-color: #235bd1;} .note--success {border-color: #17731d;} .note--warning {border-color: #965203;}
Separate CSS rulesets with empty lines
- One (1) empty line between closely related rulesets
- Two (2) empty lines between loosely related rulesets
- Five (5) empty lines between entirely new sections
/*------------------------------------*\ #FOO \*------------------------------------*/ .foo {} .foo__bar {} .foo--baz {} /*------------------------------------*\ #BAR \*------------------------------------*/ .bar {} .bar__baz {} .bar__foo {}
Quotation marks
Use single ('') rather than double ("") quotation marks for attribute selectors and property values.
body { font-family: 'Nunito', 'Arial MT Rounded Bold', Arial, sans-serif; }
Avoid shorthand properties when you don't need to set all values in a property
This avoids overriding other values encapsulated by the shorthand property, which can cause unexpected results. It applies to all properties with a shorthand: background
, border
, font
, margin
, padding
, etc.
/* Don't do this: */ .avatar { // overrides other values encapsulated by the shorthand property. // In this case, background-image and its associative properties are set to “none”. background: #ccc; border: 1px; } /* Do this: */ .avatar { background-color: #ccc; border-width: 1px; }
Always use leading zeros (0) in values
Put zeros in front of values or lengths between -1 and 1.
code, kbd, samp { font-size: 0.9em; }
Avoid using !important (except in print styles)
Using !important
is problematic because it breaks the natural cascade of CSS. Before 'going nuclear', investigate and refactor any problematic rulesets to try and lower the specificity.
In the event of a need to raise specificity to resolve a problem, use a class name chained with itself, for example:
.site-nav.site-nav {}
Because ID selectors have a greater specificity than classes, this can sometimes create issues. One way of avoiding this is to use an attribute selector instead of an ID selector, for example:
<!-- In the HTML markup --> <div id="third-party-widget">...</div> /* In the CSS */ [id="third-party-widget"] {}
Commenting
Help your future self and your colleagues by adding comments for anything that isn't immediately obvious from the code alone.
High-level comments
For large comments that document entire sections or components, use a DocBlock-ish multi-line comment. Provide a concise explanation of all non-trivial code, such as descriptions of states, permutations, conditions and treatments.
/** Use this to switch between vertical and horizontal layouts: https://www.freecodecamp.org/news/the-fab-four-technique-to-create-responsive-emails-without-media-queries-baf11fdfa848/ `.l-switcher` switches a Flexbox context between a horizontal and a vertical layout at a given, CONTAINER-BASED breakpoint, rather than using viewport media queries. The child elements in the horizontal configuration will be of equal width. For gutter spacing, apply padding to the flex items and an equal negative margin to the flex container. This will double up, so use half the intended value. */
Commenting across partial files
Our preferred CSS preprocessor is Sass using the SCSS syntax. We often split our code across multiple Sass partial files.
When working across multiple partials you will often find that rulesets that can work in conjunction with each other are not always in the same file or location.
An example from Amplify can be found in the alignments utilities partial, which includes rulesets which are dependent upon a media query breakpoint setting in a separate partial. This relationship is documented as follows:
/** Change text-alignment at the "laptop" breakpoint, as defined in /00-settings/breakpoints */ .u-text-center-from-lap { @include mq($bp-lap) { text-align: center; } }
This sort of comment can make a lot of difference to developers who are unaware of relationships across projects, or who want to understand how, why and where other styles might be inherited from.
Low-level comments
We often want to comment on specific declarations in a ruleset. To do this we use a kind of reverse footnote:
/** 1. Don't rely on colour alone for styling links - see https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html 2. Remove outline on focused links when they're also active/hovered. 3. Remove text underline from links styled as buttons. 4. Prevent `.button` going full-width if a child of a flex column. */ a:not([class]), a.with-icon--before, a.with-icon--after { border: none; color: $link-color; cursor: pointer; text-decoration: underline; /* 1 */ text-decoration-skip-ink: auto; text-underline-offset: 0.25em; &:visited { color: $link-color--visited; } &:hover { color: $link-color--hover; outline-width: 0; /* 2 */ } &:active { color: $off-black; outline-width: 0; /* 2 */ } &:focus { background-color: $focus-color; background-image: linear-gradient(to top, $black 3px, $focus-color 3px, $focus-color); color: $black; text-decoration: none; } } .button { text-decoration: none; /* 3 */ } button, .button, input[type="submit"].button { align-items: center; align-self: start; /* 4 */ background-color: $link-color; border: solid 2px $link-color; border-radius: rem(6); color: $off-white; display: inline-flex; justify-content: center; padding: 0.25em 0.75em; &:hover { background-color: $link-color--hover; border-color: $link-color--hover; color: $off-white; } &:active, &:focus { color: $black; background-color: $focus-color; background-image: none; border-color: $black; } }
This approach helps keep all documentation in one place whilst referring to the parts of the ruleset to which they belong.
Pre-processor comments
Most pre-processors provide the option to write comments that will not get compiled into the resulting CSS file. Use pre-processor comments (with //
) to document code that would not get written out to that CSS file either, such as variables:
// Dimensions of the @2x image sprite: $sprite-width: 920px; $sprite-height: 212px;
Remove all comments from production-ready CSS
In production environments ensure that all CSS is minified, resulting in loss of comments, before being deployed.
Naming conventions
Instead of presentational or cryptic class names, always use classes that reflect the purpose of the element in question, or that are otherwise generic. Names that are specific and reflect the purpose of the element are preferred. These are the most understandable and the least likely to change. Generic names are best for things such as utility and helper styles.
Follow these rules when writing class names:
- All lowercase letters
- Words/strings separated by a hyphen (-)
- Follow BEM naming conventions (more on this next)
Use BEM CSS
We use a modified version of the BEM (Block, Element, Modifier) pattern, as detailed in this post by Harry Roberts.
BEM splits components' classes into three (3) groups:
- Block: The sole root of the component
- Element: A component part of the Block
- Modifier: A variant or extension of the Block
/* Block component */ .block {} /* Element that depends upon the block */ .block__element {} /* Modifier that changes the style of the block */ .block--modifier {} /** For example: */ /* 'Alert box' component */ .alert-box {} /* Element inside the 'alert box' component used to close it */ .alert-box__close {} /* Variation of the base 'alert box' component */ .alert-box--success {}
Don't use a modifier class on its own - always use with the starting block class:
<!-- Note how both the block class and modifier class are used --> <div class="alert-box alert-box--success"></div>
JavaScript (JS) enhancements
Use a data-
attribute on any component/element requiring JavaScript enhancement, because:
- They are less likely to be mistakenly renamed, overwritten or removed than a regular class name
- They can be targeted by both JavaScript and CSS
<!-- In the HTML markup --> <button data-trigger="mobile-nav">Menu</button> <div class="card" data-component="card">...</div> /** In the CSS */ [data-trigger="mobile-nav"] {} [data-component="card"] {}
Use the js-
prefix for any class that is added to a component or element by JS. This helps to make the origin of class names clear.
Nesting
Selectors
Nesting selectors increases specificity, which means that overriding such CSS needs to be targeted with an even more specific selector. This can quickly become a significant maintenance issue, so excessive nesting is a bad idea.
Avoid nesting selectors more than three (3) levels deep, and try and limit nesting to the following scenarios:
State selectors and pseudo selectors
Such as :hover and :focus states and ::before and ::after.
a { &:hover { text-decoration: underline; } &:focus { outline: 1px dashed #000; } } span { &::before { content: '\2022'; } }
Items that must semantically sit within other elements
Such as list items and table elements.
ul, ol { margin-bottom: 1rem; margin-top: 1rem; > * + *, li ul, li ol { margin-top: 0.75em; } } dl { margin-bottom: 1rem; margin-top: 1rem; dt { font-weight: bold; } dd + dt { margin-top: 0.5em; } dt + dd, dd + dd { margin-top: 0.25em; } } table { > tr { > th { background-color: #ccc; } > td { background-color: #eee; } } }
JavaScript-only style enhancements
.component-class { display: block; .js & { display: flex; } }
Flow html elements within other elements
.component-class { span {} em {} small {} /* etc */ }
Breakpoints
Nest within the relevant parent using @include
. Do not nest styles for multiple elements within a single breakpoint.
Do not nest BEM class names
- It makes it much harder to find class names.
- It hinders developers' understanding of the context they are working on.
- When reviewing a pull request, it requires extra effort to see the actual selector and to do a proper review.
/* Don't do this: */ .alert-box { background-color: #fcfcfc; &--success { background-color: #17731d; } &__close { padding: 0.75rem; &--large { padding: 1.25rem; } } } /* Do this: */ .alert-box { background-color: #fcfcfc; } .alert-box--success {background-color: #17731d;} .alert-box__close { padding: 0.75rem; } .alert-box__close--large {padding: 1.25rem;}
Guidance on CSS selectors
Selector intent - deciding what needs to be styled and how best to select it
It is important to scope selectors correctly, and to select the right things for the right reasons. For example:
/* Don't do this */ header ul {} /* Do this: */ .global-navigation {}
Selectors should be as explicit and well-considered as the reason for wanting to select something.
Reusability
Only use element names within selectors if there is certainty about the HTML markup - i.e. it will not be subject to change - and if it helps to reinforce semantic markup.
Class names are preferred to IDs.
It is generally recommended to avoid styling things based on where they are, and to style them according to what they are instead. A reasonable exception to this rule is where a component may have specific styles according to which page template it is used within. Use your best judgement.
Type selectors
Avoid qualifying class names with type selectors, and try to avoid using element names in conjunction with classes. Qualified selectors are harder to reuse.
/* Don't do this */ ul.nav {} div.error {} /* Do this: */ .nav {} .error {}
To signal to other developers where a class might be expected or intended to be used, do this:
/*ul*/.nav {}
This tells us that the .nav
class is meant to be used on a ul
element, and not on a nav
, but the commented out leading element avoids qualifying and increasing the specificity of the selector.
Units
Unless writing a print style sheet, always use relative units - rem
or em
- for font-size
and a unitless value (e.g. 1.5
) for line-height
.
It is acceptable to use fixed units - px
or pt
- for font-size
in print style sheets.
Favour relative units over fixed units for margin and padding.