Clean, consistent and understandable CSS is paramount to a successful project. It sets the tone, both visually and architecturally, for the entire front-end. These are our collected (and evolving) best practices for writing styles with some style. 😎
This structure is an adaptation of Sass Guidelines: The 7-1 Pattern. It represents the organization of different types of style sheets, and the order they should follow. Unless specific project requirements dictate otherwise, the directory structure should look something like this:
├── base/ │ ├── forms.css │ ├── typography.css │ └── variables.css ├── components/ │ ├── alert.css │ ├── button.css │ └── grid.css ├── utils/ │ ├── display.css │ ├── position.css │ └── text.css ├── vendor/ │ └── ... └── main.css
The base/
directory should contain foundational styles and other global dependencies. There should be few (if any) class definitions within this directory. In addition to common variables, this would also be an appropriate place to put mixins or functions if a preprocessor is in use.
-
Variables, mixins and functions should be organized in their own separate files.
/* base/variables.css */ :root { --font-family-sans: popular-sans, "Helvetica Neue", sans-serif; --font-size-base: 22px; --transition-duration: 360ms; } @custom-media --width-min-md screen and (min-width: 30em); @custom-media --width-min-lg screen and (min-width: 60em);
-
Each file should only be responsible for providing base styles for a designated group of related elements.
/* base/scaffolding.css */ * {/*...*/} html {/*...*/} body {/*...*/}
/* base/forms.css */ label {/*...*/} input {/*...*/} button {/*...*/}
/* base/typography.css */ h1, h2, h3 {/*...*/} pre, code {/*...*/}
The components/
directory should have the largest number of files, because most of the CSS should be written as small, reusable components.
-
Each file should contain only one component and, if possible, only class selectors with a common namespace for that component:
/* components/alert.css */ .Alert {/*...*/} .Alert--fixed {/*...*/} .Alert-header {/*...*/} .Alert-closeButton {/*...*/}
/* components/button.css */ .Button {/*...*/} .Button--large {/*...*/} .Button--small {/*...*/}
The utilities/
directory is where definitions for utility classes (or "helpers") should occur. Unlike components, there may be many of these class definitions per file. Their organization should be according to what kind of properties they affect. See SUIT CSS Utils for a nice example of this organization.
-
Each file should contain only classes prefixed with
u-
to identity them as utilities./* utilities/margin.css */ .u-marginAbove { margin-top: var(--vspace) !important; } .u-marginBelow { margin-bottom: var(--vspace) !important; }
-
To ensure dominance over conflicting declarations, utilities should come last after all other styles in
main.css
./* main.css */ @import "base"; @import "components"; @import "sections"; @import "utils";
These rules were adapted from CSS Guidelines. This is an example of how declaration block syntax should be formatted (Sass syntax used to illustrate nesting):
.Selector-1,
.Selector-2 { /* 1 */
@include some-mixin(); /* 2 */
display: block; /* 3 */
background-color: rgba(0, 0, 0, 0.1); /* 4 */
background-image: url("image.png"); /* 5 */
opacity: 0.8; /* 6 */
padding: 0; /* 7 */
transition-duration: 0.3s; /* 8 */
&:hover,
&:focus { /* 9 */
color: green;
}
&.is-disabled,
&[disabled] { /* 10 */
pointer-events: none;
}
@media (min-width: 40em) { /* 11 */
width: auto;
}
&::before { /* 12 */
content: "Foo";
}
} /* 13 */
/* 14 */
- Combined selectors should be on separate lines, and the opening brace should be on the same line as the last selector.
- Variables and preprocessor cruft should come before any properties, separated with blank line below.
- One space should be after each colon, and each line should end with a semicolon.
- Comma-delimited numbers inside of values with parenthesis (e.g.
rgb(0, 0, 0)
) should have spaces between them. - Strings within values should use double quotes.
- Decimal values should include a leading
0
. - Values of zero should be unit-less.
- Time values should be represented with the second (
s
) unit. This is easier for humans to parse, and encourages shorter values that divide evenly within an ideal 60 frames-per-second animation speed. - Nested pseudo class blocks should come after all property declarations.
- Nested class or attribute blocks should come after all pseudo class blocks.
- Nested media queries should come after all combined class or attribute blocks.
- Nested pseudo element blocks should come after all media queries.
- Closing braces should be on a new line.
- A blank line should come after each declaration block.
We use a standard order for nested selectors in Sass. By following this order, our code becomes more standardized and easier to understand.
When in doubt, remember that the goal is to first define any included styles, followed by regular styles, followed by modifiers, followed by children (whose code would follow this same order).
-
-
Local
$variables
- Local variables should really only be declared for mixins or functions. Most variables should be kept in the main variables partial, since variables in Sass are global.
- However, if you have a set of variables that will only be used in a certain partial, declare them at the top of that file and prefix their names to avoid confusion in the global namespace. e.g.,
$m_module_name_height
.
-
@extend
statements- Avoid extends in favor of mixins. Extends offer no benefits over mixins, and can cause specificity problems.
-
@include
statements- Knowing right off the bat that this class inherits another whole set of rules from elsewhere is good. Another benefit is that overriding styles for that inherited set of rules becomes much easier.
-
-
-
Adding regular styles after the
@extend
and@include
statements allows us to override those properties, if needed. Be sure to leave a comment if a declaration's only purpose is to override included styles. -
Alphabetizing is helpful for a large team because it standardizes location of properties (See "Property Sorting" below).
-
-
-
Pseudo-classes (
:hover
), attribute selectors (&[type="input"]
), and state classes (&.is-active
)- These directly modify the parent selector so we declare them before other nested selectors.
- Most BEM modifier classes don't need to be nested. However, some generic state class names like
is-active
that would otherwise be too non-specific to live in the global namespace, should be nested here.
-
Nested media queries
- These come after regular styles, pseudo-classes, etc., so they can override them.
-
Parent modifier classes (
.parent__modifier &
)- Modifiers on parents can affect children. They should be nested like media queries.
-
-
-
Pseudo-elements (
::before
)- Pseudo-elements are actually generated children, and so should be treated like any other nested selector.
-
Nested selectors
- As a rule, if a selector will work without it being nested then do not nest it.
- There shouldn’t be many nested selectors. Our naming convention means that we don’t need to nest selectors for namespacing. Before you nest a selector, consider if it would be better as a child class or modifier class.
- Any nested selectors could contain their own nested pseudo-classes, pseudo-elements and media queries, following the rules above.
-
.foo {
@include font_ui(2.0); // 1.iii
line-height: 1; // override font_ui line-height, 2.i
width: 100%; // 2.ii
&:hover { // 3.i - modifier: pseudo-class
color: red;
}
// state classes usually have `is-` or `has-` class names that need to be nested.
&.has-error { // 3.i - modifier: state class
color: red;
}
@media only screen and (min-width: $screen_medium) { // 3.ii - modifier: media query
width: 100%;
}
@media only screen and (min-width: $screen_large) { // 3.ii - modifier: media query
width: 50%;
}
&::before { // 4.i - child: pseudo-element
content: "!";
}
// this should really be a non-nested BEM child class!
h2 { // 4.ii - child: nested selector
font-size: 2em;
}
}
// modifier with proper BEM name
// note the nested selectors follow the same order.
.foo--bar {
color: red;
&:hover {
color: orange;
}
@media only screen and (min-width: $screen_large) {
display: none;
}
}
// child element with proper BEM name
.foo__icon {
width: 1em;
// nested modifier makes it easier to understand
.foo--bar & {
width: 2em;
}
}
Until recently, we followed Guy Routledge's Outside In method of property sorting. Over time, we discovered most property sorting methodologies require maintenance as new properties (for example, flex-*
or grid-*
) are introduced. This makes them inherently fragile and difficult to enforce.
We now recommend properties are sorted alphabetically. This technique will scale to accommodate any new properties, and requires no learning curve for new project contributors.
- You can quickly alphabetize declarations in Sublime Text by selecting several lines of code and pressing F5.
- You can quickly alphabetize declarations in VS Code by selecting several lines of code and using the "Sort Lines Ascending" command. This has no keyboard shortcut by default, so you can either use the command pallete (CMD+SHIFT+P) or assign a keyboard shortcut.
/* Do */
selector {
display: flex;
font-size: 2em;
left: 0;
position: absolute;
top: 0;
}
/* Don't */
selector {
position: absolute;
top: 0;
left: 0;
display: flex;
font-size: 2em;
}
It is recommended to use tools like stylelint to enforce these sorts of rules, which can be combined with the --fix
option or a complementary tool like stylefmt to organize CSS properties from text editors like Atom and Sublime Text.
-
ID selectors should be avoided
/* Don't */ #some-id {}
/* Don't even */ element#some-id {}
-
Element selectors should be avoided outside of defining base styles
/* Don't */ .Component ul {}
/* Don't */ .Component > span {}
/* Don't */ li.Component-item {}
-
Nesting should be avoided unless the resulting specificity is actually needed
/* Do */ .Component {} .Component-element {}
/* Don't */ .Component {} .Component .Component-element {}
Note: It is also acceptable to "nest" pseudo classes and pseudo elements when using a preprocessor, since doing this has no unintentional effect on specificity.
-
Nested selectors may only be two levels deep and must consist of a modifier class followed by an element class
.Component--modifier .Component-element {}
-
Unrelated selectors should not be combined
/* Do */ h1, h2, h3 { font-weight: bold; letter-spacing: -0.1em; }
/* Don't */ .Component-header, .OtherComponent-header, .AnotherComponent--extended { margin-top: 0; }
Note: If using a preprocessor like Sass, the
@extend
feature should be used with great care to avoid the unintentional combining of unrelated selectors.
Naming stuff can be hilariously difficult. To minimize future confusion, try to pick names that are less likely to change (or become inaccurate). For example, a component name of .FormGroup
might be better than .RegistrationShipping
, since it describes a function rather than a context. See Naming CSS Stuff Is Really Hard for more examples.
The following conventions were adapted from the SUIT CSS Naming Conventions.
Syntax: u-[sm|md|lg-]<utilityName>
.u-floatLeft {
float: left !important;
}
@media (--sm) {
.u-sm-floatLeft {
float: left !important;
}
}
Syntax: <ComponentName>[--modifierName|-descendentName
/* Component Name */
.Alert {}
/* Modifier */
.Alert--dismissable {}
/* Descendent */
.Alert-closeButton {}
/* Modifying descendants */
.Alert--dismissable .Alert-closeButton {}
Avoid nesting descendents:
/* Do */
.Nav-subnavButton {}
/* Don't */
.Nav-subnav-button {}
If you find yourself wanting complex parent-child relationships within a single component, you may benefit from breaking it into into multiple independent components:
.Nav {}
.Subnav {}
Syntax: <ComponentName>.is-stateOfComponent
State classes should reflect changes to a component's state. As such, it's important to resist the temptation to apply direct styles to these classes. Scoping states to associated component classes insures the use of consistent cross-component terminology.
/* Component Name */
.Tweet {}
/* State of component */
.Tweet.is-expanded {}
It's also acceptable (and in some cases preferred) to manage state via built-in browser properties:
.Toggle:checked {}
.Input:disabled {}
.Tweet[aria-expanded="true"] {}
When deciding how to construct complex class names with multiple delimited pieces, look at existing HTML specifications for inspiration before deciding on something arbitrary.
The ARIA Role, State, and Property Quick Reference is a good resource for common element and state names.
/* Elements */
.Component-body {}
.Component-header {}
.Component-button {}
.Component-dialog {}
.Component-img {}
/* Modifiers */
.Component--labelled {}
.Component--hasPopop {}
/* States */
.Component.is-busy {}
.Component.is-expanded {}
.Component.is-checked {}
.Component.is-selected {}
See CSS Guidelines: JavaScript Hooks
Comments are a good idea. There is no standardized CSS documentation tool (KSS for example) currently in use. For comments that explain declarations, this formatting is nice:
From https://github.com/suitcss/components-arrange/blob/master/lib/arrange.css
/**
* 1. Protect against the component expanding beyond the confines of its
* container if properties affecting the box-model are applied to the
* component. Mainly necessary because of (5).
* 2. Rely on table layout.
* 3. Zero out the default spacing that might be on an element (e.g., `ul`).
* 4. Make sure the component fills at least the full width of its parent.
* 5. Reset the table-layout algorithm in case a component is nested.
*/
.Arrange {
box-sizing: border-box; /* 1 */
display: table; /* 2 */
margin: 0; /* 3 */
min-width: 100%; /* 4 */
padding: 0; /* 3 */
table-layout: auto; /* 5 */
}
Avoid simply restating what the property already tells us. Try to provide useful clarification of why a rule was chosen.
/* Don't */
/**
* 1. Use relative positioning.
*/
.Nav {
position: relative; /* 1 */
}
/* Do */
/**
* 1. Allow child elements like the logo to position themselves relative
to this container.
*/
.Nav {
position: relative; /* 1 */
}
See CSS Guidelines: Architectural Principles
Use the following properties with care. They should occur in reusable utilities most of the time, and not repeated across a wide variety of components.
margin
float
width
height
font-*
line-height
text-align
white-space
When writing base styles for a component, assume that the component is unaware of everything outside of its box. Add styles that depend on surrounding elements with care (or instead use more utility classes in your HTML.)
/* Do */
.Component {
display: block;
}
.Component--withMargin {
margin-top: 1em;
}
.Component--size1of2 {
width: 50%;
}
/* Don't */
.Component {
margin-top: 1em;
display: block;
width: 50%;
}
When combining components for specific overrides, do this in a context-specific style sheet:
/* sections/search.css */
.Section--search .Button {/*...*/}
If this combination reoccurs, consider creating a new component to abstract the needed pieces:
/* components/searchbar.css */
.SearchBar {/*...*/}
.SearchBar-button {/*...*/}
Vendor prefixes or other non-standard fallbacks make CSS difficult to read and maintain. Even mixins require documentation and maintenance. Autoprefixer will handle prefixes and other fallbacks based on your project's actual support requirements. This allows us to write styles in a standard and predictable way:
/* Don't */
.Component {
-webkit-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%);
}
/* Don't */
.Component {
@include translateX(-50%);
}
/* Do */
.Component {
transform: translateX(50%);
}
- stylelint With this plugin, you can check the validity of stylesheets against a set of project-specific conventions. The plugin will throw an error if it finds CSS that violates these rules.
- stylefmt Works alongside stylelint to help format your code.
- The CSS Specificity Graph A very simple model for diagrammatically assessing the overall health of your codebase in terms of specificity—a way of looking at an entire project’s CSS and highlighting any potentially troublesome areas of higher-than-ideal specificity.
- CSS Dig A Chrome extension that summarizes selector and propery usage for any given page. Particularly useful for identifying areas of needless repetition (see Dryness).