CSS 💅
Formatting
-
Use soft-tabs with a two space indent.
-
Use the SCSS syntax. So not SASS.
-
Use one space between selector and
{
. -
Use one space between property and value.
width:20px;
width: 20px;
-
Don’t add a unit specification after 0 values, unless required by a mixin.
-
Use a leading zero in decimal numbers.
opacity: .5;
opacity: 0.5;
-
Avoid using shorthand properties for only one value.
background: #f00;
background-color: #f00;
-
Use SCSS object-based shorthand properties for multiple values.
background-color: #f00; background-image: url(...); background-size: cover;
background: { color: #f00; image: url(...); size: cover; };
-
Use space around the operator.
$variable*1.5
$variable * 1.5
-
Use parentheses around individual operations in shorthand declarations.
padding: $variable * 1.5 $variable * 2;
padding: ($variable * 1.5) ($variable * 2);
-
Use double colons for pseudo-elements.
element:after {}
element::after {}
-
Use a blank line above a selector that has styles.
.block { ... } .block__element { ... }
.block { ... } .block__element { ... }
-
Prefer lowercase hex color codes. For readability, pick the shorter version (3 digits) of hex color codes whenever possible.
color: white;
color: #FFF; color: #1968C7;
// Better color: #fff; color: #1968c7;
-
Avoid nesting code more than 3 levels deep as 1) it improves readability since it forces to break large pieces of code into smaller units and 2) it diminishes the risks to create over-qualified selectors when it’s not necessary.
.block { &__element { h1 { ... span { ... } } } }
.block { &__element { h1 { ... } // or h1 span if it's necessary span { ... } } }
-
Prefer breaking down large pieces of CSS code into smaller units. It’s not because CSS code can be nested that all code for a block or element needs to be nested into a giant block. There is no rule that works in 100% of cases, but 1) use the HTML structure for the ordering of selectors and 2) use common sense to group blocks and elements as meaningful units.
.block { ... &__element1 { ... } &__element2 { ... } [...] &__element15 { ... } }
.block { ... } .block__element1 { ... } .block__element2 { ... }
-
Use SCSS comments blocks with two forward slashes
//
. As a general rule, place comments above the line it concerns./* this is a comment */
// this is a comment
-
Use unquoted strings rather than quoted strings for map keys.
$font-weights: ( 'regular': 400, 'medium': 500, 'bold': 700, 'extra-bold': 900 ); $font-weights: ( "regular": 400, "medium": 500, "bold": 700, "extra-bold": 900 );
$font-weights: ( regular: 400, medium: 500, bold: 700, extra-bold: 900 );
-
Prefer looking up a value with single quotes when using the Maps function.
map-get($font-weights, medium); map-get($font-weights, "medium");
map-get($font-weights, 'medium');
Naming
Files
-
Use kebab case for file names:
media_lightbox.css mediaLightbox.css
media-lightbox.css media-lightbox.scss
-
Add a prefix underscore
_
in the name to the files which are imported using@import
. However, omit the prefix underscore_
when importing these files with@import
.components/ ├── media-lightbox.scss // In the application.scss @import 'components/_media-lightbox'
components/ ├── _media-lightbox.scss // In the application.scss @import 'components/media-lightbox'
-
Do NOT add a prefix underscore
_
to manifest files (ex:application.scss
) or stylesheets that are directly imported into a web page.components/ ├── _media-lightbox.scss _application.scss
components/ ├── _media-lightbox.scss application.scss
-
Do NOT add the file extension (ex:
.scss
) when importing files with@import
.components/ ├── _media-lightbox.scss // In the application.scss @import 'components/media-lightbox.scss'
components/ ├── _media-lightbox.scss // In the application.scss @import 'components/media-lightbox'
Class Names
-
Use kebab case for class names.
-
Class names must be composed of at least two words to make each element class name specific and more descriptive.
.dropdown {} .navigation {}
.navigation-dropdown {} .navigation-primary {}
-
Use the UI element type as a prefix following by one to two words. While it might seem counter-intuitive at first, this technique makes it easier to differentiate component purpose and search through components. Most framework use this technique e.g.
btn-primary
is not calledprimary-btn
asbtn
is the namespace grouping all buttons.<form class="search-form"></form> <ul class="product-list"></ul> <div class="product-card"></div>
<form class="form-search"></form> <ul class="list-product"></ul> <div class="card-product"></div>
Commonly used UI elements types are
accordion
,button
,card
,carousel
,form
,list
,navigation
,modal
,notification
,pagination
,slider
,tooltip
. -
Do NOT use HTML tag name as class names. Consider tag names are reserved keyword.
// Bad .h1 {} .nav {} .card-product > .header {}
.display-heading {} .navigation-menu {} .card-product__header {}
-
Keep all class names to singular to avoid having some in plural while other in singular. This reduces cognitive fatigue and chances of using a wrong selector.
.list-projects {} .form-orders {}
.list-project {} .form-order {}
-
The block class name defines the namespace for its elements and modifiers.
// .card-product is the block class name .card-product { &__header {} &__body {} &__footer {} &--has-border {} }
-
The element class name is separated from the block name by a double underscore
__
..card-product { // .card-product__header is an element from the block .card-product &__header {} // .card-product__body is an element from the block .card-product &__body {} // .card-product__footer is an element from the block .card-product &__footer {} }
-
The modifier name is separated from the block or element name by a double hyphen
--
..card-product { // .card-product--has-border is a modifier from the block .card-product &--has-border {} // .card-product__header--secondary is a modifier from the element .card-product__header &__header--secondary {} }
-
When nesting, an element is always part of a block, not another element. This means that element names can’t define a hierarchy such as
block__elem1__elem2
.<form class="form-search"> <div class="form-search__content"> <input class="form-search__content__input"> <button class="form-search__content__button">Search</button> </div> </form>
<form class="form-search"> <div class="form-search__body"> <input class="form-search__input"> <button class="form-search__button">Search</button> </div> </form>
-
Use
$self
to reference the parent block class name when writing complex components instead of repeating the block class name..card-campaign { $self: &; &__body { //... .card-campaign--completed .card-campaign__value { display: flex; } } }
.card-campaign { $self: &; &__body { //... #{ $self }--completed #{ $self }__value { display: flex; } } }
SCSS
Use kebab case when naming mixins, functions & variables.
$color_blue: blue;
@mixin spanColumns() {}
$color-blue: blue;
@mixin span-columns() {}.
Stylesheets Structure
The team’s architecture is heavily inspired by SMACSS, with some variations based on their experience and usage in projects.
base/
├── _buttons.scss
├── _fonts.scss
├── _forms.scss
├── _layout.scss
├── _list.scss
├── _media.scss
├── _table.scss
├── _typography.scss
components/
├── _app-navigation.scss
├── _button-hamburger.scss
├── _logo.scss
functions/
├── _asset-url.scss
├── _image-url.scss
layouts/
├── default.scss
├── authentication.scss
├── error.scss
mixins/
├── _text-truncate.scss
screens/
├── home.scss
├── login.scss
theme/
├── _filestack.scss
├── _pygments.scss
-
base/
: The overrides of built-in HTML tags and selectors. These are usually normalization styles or an addendum to an external normalization stylesheet such as Normalize. -
components/
: The custom components created for the application. 90% of all stylesheets code is generally located in this folder. -
functions/
: SCSS functions. -
layouts/
: The distinct and shared page layout styles. -
mixins/
: SCSS mixins. -
screens/
: The overrides or custom styles (not shared) are required on a screen basis. -
vendor/
: Overrides of third-party modules styles. Since these components were not created by the team, the styles are simply overridden. -
./
: The root of the styles folder contains shared config e.g._variables.scss
, generated files e.g._icon-sprite.scss
and the CSS manifest file e.g.application.scss
The CSS files need to be imported in this order in the manifest file:
// Import dependencies
@import '~/node_modules/normalize/';
@import 'variables';
// Other generated files
@import 'icon-sprite.scss';
@import 'functions/*';
@import 'mixins/*';
@import 'base/*';
@import 'layouts/*';
@import 'components/*';
@import 'screens/*';
@import 'vendor/*';
The parent folder must be named
stylesheets/
following the Ruby on Rails convention. Sostyles/
orcss/
are not valid.
Base
The following files are usually required:
-
_buttons.scss
: Default styling for<button>
and<input>
of typesbutton
,reset
andsubmit
. These selectors are placed outside ofbase/_form.scss
as<button>
tags can be placed of forms. -
_fonts.scss
: Definition of custom@font-face
. -
_forms.scss
: Default styling for<form>
,<input>
(except of buttons, see above),<select>
,<textarea>
,<legend>
. -
_layout.scss
: Default styling for<html>
and<body>
. -
_list.scss
: Default styling for<ul>
,<ol>
and<dl>
. -
_media.scss
: Default styling for<img>
,<figure>
and<i>
(when used for icons). -
_table.scss
: Default styling for<table>
and related tags e.g<td>
,<th>
… -
_typography.scss
: Default styling for text content e.g.<h1>
,<a>
or<p>
.
Avoid targeting selectors using class names in
base/
but instead the raw tags.
Components
-
Each file must contain only one component.
-
Each component is namespaced by a BEM block name. The file name must match the block name in snake case.
// File is named "button-hamburger.scss" .button-hamburger { ... }
-
Each styles block must be prefixed by the block element name.
.button-hamburger { ... } .text-fallback { ... }
.button-hamburger { ... } .button-hamburger .button-hamburger__text-fallback { ... }
-
Use
@extend
to re-map a framework component class name to a custom BEM class name..form__control { @extend .form-control; } .text--muted { @extend .text-muted; }
Functions
-
Each file must contain only one function.
-
The file name must match the function name in snake case.
-
Prepend the function code by a doc block with
input
andoutput
details.// File is named "asset-url.scss" // Generate urls for an asset file // // @param {String} $file - The path to the file // @return {String} - The url property value @function asset-url($file_path) { ... }
Layouts
-
A layout is the shared page structure of a page/screen. Think that there are some pages with a single column layout while others with a two-column layout (a sidebar and a main area). In this case, there would be two layout files.
-
At the minimum it must contain one application-wide layout called
default.scss
. The application layout is the one that is the most widely used in the application. -
The layout class name must be placed on the top most tag which is
<html>
. -
Target only top level selectors (close to
<body>
) that are used to create the layout.<html class="layout-default"> <body> <aside class="app-sidebar"> <nav class="app-nav"> ... </nav> </aside> <main class="app-content"> <ul class="list-project"></ul> ... </main> </body> </html>
Then the layout file must only contain these selectors:
.layout-default { .app-sidebar { ... // No styles for .app-nav here as it does not impact the layout } .app-content { ... // No styles for .list-project as it does not impact the layout } }
Creating the layout styles must be done first. Therefore, it’s important to dedicate enough time identifying all the possible layouts in an application before moving to creating components or screens.
Mixins
-
Each file must contain only one mixin.
-
The file name must match the mixin name in snake case.
// File is named "text-truncate.scss" @mixin text-truncate() { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis; }
Screens
-
Each file should target only one screen. In some rare cases, it could target a group of screens e.g.
authentication.scss
would targetlogin
,registration
andforget-password
. But it would still be preferable to have three fileslogin.scss
,registration.scss
andforget-password.scss
. -
The file name must match the screen name in snake case.
-
The screen class name is usually placed on the second top most tag which is
<body>
.<html class="layout-default"> <body class="home"> <aside class="app-sidebar"> <nav class="app-nav"> ... </nav> </aside> <main class="app-content"> <ul class="list-project"></ul> ... </main> </body> </html>
-
Each styles block must be prefixed by the screen block name.
.home { .app-sidebar { ... } }
Vendor
-
This folder can be omitted if the project does not require overriding third party modules styles.
-
The file name must match the module name in snake case.
-
Make sure that the selectors do not conflict with the application code.
Responsive Styles
Follow a mobile-first approach in all projects. This assumes that all code first works on small/mobile screens, then be overridden in order the ascending order of screen width breakpoints:
.block {
// shared code for all screens sizes or specific to small/mobile screens
@media only screen and (min-width: 768px) {
// Overrides or specific styles for table/phablet screens
}
@media only screen and (min-width: 1140px) {
// Overrides or specific styles for desktop screens
}
}
-
Do not wrap several selectors in a media query block, instead place each media query into each selector. It allows to group all styles for a selector across all screen sizes which is easier to manage.
.block1 { ... } .block2 { ... } @media only screen and (min-width: 768px) { .block1 { ... } .block2 { ... } }
.block1 { ... @media only screen and (min-width: 768px) { ... } } .block2 { ... @media only screen and (min-width: 768px) { ... } }
-
Do not hard code breakpoints values but instead store them in shared variables. An even better solution is to use a mixin to generate the media query block.
.block1 { ... @media only screen and (min-width: 768px) { ... } } .block2 { ... @media only screen and (min-width: 768px) { ... } }
// In _variables.scss $grid: ( tablet: 768px, desktop: 1140px ); // In other files .block1 { ... @media only screen and (min-width: map-get($grid, 'tablet')) { ... } } .block2 { ... @media only screen and (min-width: map-get($grid, 'tablet')) { ... } }
// Better // In mixins/_media-breakpoint.scss @mixin media-breakpoint($size) { @media only screen and (min-width: $size) { @content; } } // In other files .block1 { ... @include media-breakpoint(map-get($grid, 'tablet')) { ... } } .block2 { ... @include media-breakpoint(map-get($grid, 'tablet')) { ... } }
Frameworks
While not compulsory, using a CSS framework like Bootstrap is usually the norm for efficiency:
- Bootstrap reboot based on Normalice.cssDefault reset of cross-browser styles e.g.
-
-
But frameworks also have downsides, such as file size and unused code among others. So to protect the application from these downsides, the team imposes restrictions and conventions on their usage of frameworks.
-
Do NOT import the whole framework but instead pick what the project requires.
// Import the manifest without any modification @import 'bootstrap/bootstrap';
// Modify the manifest file by commenting out what's not used @import "functions"; @import "variables"; @import "mixins"; @import "reboot"; @import "utilities"; @import "print"; // Components @import "grid"; @import "card"; // Re-enable these if needed. // @import "root"; // @import "buttons"; // @import "images"; // @import "code"; // ... // @import "spinners";
-
Use frameworks styles in CSS instead of using the framework helper classes in the HTML code:
- The framework does not leak to the HTML which allows for a clear separation between markup and styles thus keeping the original purpose of HTML and CSS 🤗.
- Usually reduces the amount of markup as most framework requires to add multiple level of nested
divs
. - Re-use the underlying implementation of the framework e.g. Bootstrap grid
row-
andcol-
classes use the utilitiesmake-row()
andmake-col()
. - Keep the BEM naming consistent across the codebase.
<body class="users show"> <main class="container"> <div class="user-profile row"> <div class="user-profile__header col col-6">Username</div> <div> </main> </body>
<body class="users show"> <div class="user-profile"> <div class="user-profile__header">Username</div> <div> </div>
.users.show { main { @include make-container(); } .user-profile { @include make-row(); &__header { @include make-col(6); } } }