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.

    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 called primary-btn as btn 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;
        }
      }
    }
    

    The team currently follows the BEM naming conventions:

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

    The team used to follow SMACSS, which also has the concept of state/variations similar to the modifier of BEM. But to prevent naming fatigue, the team opted for 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.

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

Ruby on Rails conventions

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.

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

    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

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:

  • 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, 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- and col- classes use the utilities make-row() and make-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);
        }
      }
    }
    

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.