CSS πŸ’…

Hero image for 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.

// Bad
width:20px;

// Good
width: 20px;
  • Don’t add a unit specification after 0 values, unless required by a mixin.

  • Use a leading zero in decimal numbers.

// Bad
opacity: .5;

// Good
opacity: 0.5;
  • Avoid using shorthand properties for only one value.
// Bad
background: #f00;

// Good
background-color: #f00;
  • Use SCSS object-based shorthand properties for multiple values.
// Bad
background-color: #f00;
background-image: url(...);
background-size: cover;

// Good
background: {
  color: #f00;
  image: url(...);
  size: cover;
};
  • Use space around the operator.
// Bad
$variable*1.5

// Good
$variable * 1.5
  • Use parentheses around individual operations in shorthand declarations.
// Bad
padding: $variable * 1.5 $variable * 2;

// Good
padding: ($variable * 1.5) ($variable * 2);
  • Use double colons for pseudo-elements.
// Bad
element:after {}

// Good
element::after {}
  • Use a blank line above a selector that has styles.
// Bad
.block {
  ...
}
.block__element {
  ...
}

// Good
.block {
  ...
}

.block__element {
  ...
}
  • Prefer lowercase hex color codes. For readability, pick the shorter version (3 digits) of hex color codes whenever possible.
// Bad
color: white;

// Good
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.
// Bad
.block {

  &__element {
     h1 {
        ...
        span {
        ...
        }
     }
  }
}

// Good
.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 we can nest code 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.
// Bad
.block {
  ...
  
  &__element1 {
    ...
  }
  
  &__element2 {
    ...
  }
  
  [...]
  
  &__element15 {
    ...
  }
}

// Good
.block {
  ...
}

.block__element1 {
   ...
}

.block__element2 {
  ...
}
  • Use SCSS comments blocks with two forward slashes //. As a general rule, place comments above the line it concerns.
// Bad
/* this is a comment */

// Good
// this is a comment

Naming

Files

  • Use kebab case for file names:
# Bad
media_lightbox.css
mediaLightbox.css

# Good
media-lightbox.css
media-lightbox.scss
  • Add a prefix underscore _ to files which are imported using @import. Manifest files or stylesheets imported directly into a web page (via a link meta tag) must not be prefixed by an underscore.
components/
β”œβ”€β”€ _media-lightbox.scss
application.scss
// In the manifest file: 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.

// Bad
.dropdown {}
.navigation {}

// Good
.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 called primary-btn as btn is the namespace grouping all buttons.
<!--Bad-->
<form class="search-form"></form>
<ul class="product-list"></ul>
<div class="product-card"></div>

<!--Good-->
<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 {}

// Good
.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.
// Bad
.list-projects {}
.form-orders {}

// Good
.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.
# Bad
<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>

# Good
<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.
// Bad
.card-campaign {
  $self: &;

  &__body {
    //...

    .card-campaign--completed .card-campaign__value {
      display: flex;
    }
  }
}

// Good
.card-campaign {
  $self: &;

  &__body {
    //...

    #{ $self }--completed #{ $self }__value {
      display: flex;
    }
  }
}

We currently follow the BEM naming conventions:

.block-name__element-name--modifier-name {}

We used to follow SMACSS which also has the concept of state/variations similarly to the modifier of BEM. But to prevent naming fatigue, we opted with a clearer system like BEM.

Be consistent about naming conventions for classes. For instance, if a project is using BEM, continue using it, and if it’s not, do not introduce it.

SCSS

Use kebab case when naming mixins, functions & variables.

// Bad
$color_blue: blue;
@mixin spanColumns() {}

// Good
$color-blue: blue;
@mixin span-columns() {}.

Stylesheets Structure

Our architecture is heavily inspired by SMACSS with some variations based on our 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) required on a screen basis.

  • vendor/: Overrides of third party modules styles. Since these components were not created by us, we simply override the styles.

  • ./: 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. So styles/ or css/ are not valid.

Base

The following files are usually required:

  • _buttons.scss: Default styling for <button> and <input> of types button, reset and submit. These selectors are placed outside of base/_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.
// Bad
.button-hamburger {
  ...
}

.text-fallback {
  ...
}

// Good
.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;
}

SCSS @extend has limitations and downsides so its use should be highly restricted to the aforementioned technique.

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 and output 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 target login, registration and forget-password. But it would still be preferable to have three files login.scss, registration.scss and forget-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

We follow a mobile first approach in all projects. This assumes that all code first work on small/mobile screens then we override 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.
// Bad
.block1 {
  ...
}

.block2 {
  ...
}

@media only screen and (min-width: 768px) { 
  .block1 {
    ...
  }
  
  .block2 {
    ...
  }
}


// Good
.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.
// Bad
.block1 {
  ...
  @media only screen and (min-width: 768px) { 
    ...
  }
}

.block2 {
  ...
  @media only screen and (min-width: 768px) { 
    ...
  }
}

// Good
// 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:

  • Default reset of cross-browser styles e.g. Bootstrap reboot based on Normalice.css
  • Generic re-usable and components without the need to re-invent the wheel
  • Set of utilities e.g. responsive styles, font sizing, animations…

But frameworks also have downsides such as file size and unused code among others. So to protect the application from these downsides, our usage of frameworks comes with restrictions and conventions.

  • Do NOT import the whole framework but instead pick what the project requires.
// Bad
// Import the manifest without any modification
@import 'bootstrap/bootstrap';

// Good
// 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- and col- classes use the utilities make-row() and make-col().
    • Keep the BEM naming consistent across the codebase.
<!--Bad-->
<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>

<!--Good-->
<body class="users show">
  <div class="user-profile">
    <div class="user-profile__header">Username</div>
  <div>
</div>
// Good
.users.show {
  main {
    @include make-container();
  }

  .user-profile {
    @include make-row();

    &__header {
      @include make-col(6);
    }
  }
}

Framework helper utilities are not all evil and have their place. These should be actually favoured for fast prototyping or ephemeral development (e.g. campaign pages). But when building application layouts and implementing design systems, the aforementioned technique must be used.