CSS

A robust approach to structuring modular CSS leads to code that is both performant and easy to maintain.

Writing modular CSS

Sass

We use Sass with the .scss syntax. Sass was chosen over Less and Stylus because it is the default for Rails and its increasing ubiquity in the developer community.

We tend to avoid Compass and Bourbon because of compilation speed. If you need mixins from these libraries it's often easier to copy them into your mixins file directly.

Some additional guidelines:

  • Only extend %placeholder selectors and never extend a standard class selector.
  • Implement OOCSS patterns (such as the media object) using placeholder selectors to hide the pattern classes from the HTML.
  • Use autoprefixer rather than mixins for browser prefixes.

Conventions

There are many similar approaches to writing modular CSS. We borrow the parts we like from:

  • OOCSS modularity and patterns e.g. the media object.
  • BEM for consistent, obvious class names (blocks, child elements and modifiers).
  • SMACSS for separating base, layout and component styles.

Architecture

Breaking apart page designs into elements, layouts and components leads to a logical file structure. For example:

/stylesheets/
  /base
    fonts.scss
    form.scss
    layout.scss
    table.scss
    typography.scss
  /components
    icons.scss
    masthead.scss
    video_player.scss
  /layouts
    base.scss
    layout_6_6.scss
    layout_12.scss
  /utilities
    mixins.scss
    variables.scss
  /vendor
    jquery.carousel.scss
    reset.scss
  application.scss

Base

Core styles live here such as fonts for @font-face declarations and layout for core page structure.

Other files in base should group together un-classed HTML elements so that they all have some default styles. For example typography should contain definitions for all heading levels, paragraphs, lists, hyperlinks, strong, emphasis etc. Assume you are styling HTML from a WYSIWYG editor or Markdown file.

  • Use rem as your typographic unit with a px fallback for legacy IE.
  • Any sizing calculations (e.g. line-height) should be generated using a mixin and not hard-coded constants.
  • Use margin-bottom and relative units to vertically space typographic elements, not margin-top or line-height.
  • Consider placeholders such as %h1 so that core typographic styles can be applied to other HTML elements (e.g. to both h1 and .my-header).

Components

Components are discrete parts of user interface. Keep rules relating to a component to one file. Use class selectors and the hyphenated BEM class naming convention.

The BEM convention improves readability and allows you to mix markup patterns without naming conflicts.

  • Try not to nest selectors more than a few levels deep.
  • Use class selectors liberally and ID selectors sparingly.
  • Use verbose class names instead of abbreviations i.e. .checkbox not .cb.
  • Keep class names lowercase and replace spaces with hyphens.
  • Name components with singular nouns (e.g. button).
  • Avoid naming components based on appearance or content as it limits re-use.
  • Avoid the &--modifier Sass syntax, instead use the full class name. This aids searchability without sourcemaps.
  • Use .js-* classes for attaching JavaScript behaviour, so do not style these.
<div class="my-component my-component--alternative">
  <h2 class="my-component__heading">...</h2>
  <div class="my-component__body">...</div>
</div>
// hyphens for spaces
.my-component { ... }

// double-hyphens for modifiers
.my-component--alternative { ... }

// double-underscores for children
.my-component__heading { ... }

Grids and Layouts

Components should generally expand to fill the width of their container. This allows us to place components within layouts which give the page structure.

We've found that the neatest way to implement a grid system is to use layouts, named after their column arrangements. There are semantic distinctions between the terms grid, column, container and layout:

  • A grid is a design construct used to arrange the interface in a consistent manner (e.g. a 12-column grid).
  • A column is a unit of the grid (e.g. a 12-column grid that is 960px wide will have a grid column width of 80px).
  • A container is a structural HTML element that wraps components.
  • A layout is an arrangement of one or more containers, each having a width of multiple columns (e.g. a "6-6" layout of a 12-column grid would be two columns each 50% wide).

Exposing a grid via layouts means there are no grid classes in markup. You could use Susy for this, but it's often easier to go native. Our default approach is to use fluid-responsive layouts, therefore:

  • Define layout container widths using percentages not px (fixed) or em (elastic).
  • Add media queries at specific widths to change the container arrangements.
  • The layout controls a component's width, so avoid putting widths on components themselves. If a component needs to readjust at specific sizes, try elementQuery. See Responsive design.
  • Avoid setting a height on anything other than icons or images.

Utilities

Utilities are for your Sass internals and shouldn't generate CSS output.

We have conventions for defining colours and fonts in the variables file, and a collection of mixins and functions for accessing the values. See a recent project for specific mixins, since this approach is evolving.

$colours: (
  'black': #000,
  'blue': #116bfb,
  ...
);

$breakpoints: (
  'large': 700px,
  'medium': 600px,
  'small': 500px,
);

$font-families: (
  'light': ('Univers Light', Arial, sans-serif),
  'regular': ('Univers Regular', Arial, sans-serif),
  'bold': ('Univers Bold', Arial, sans-serif),
);

$font-sizes: (
  'small': (font-size: 16, line-height: 1.333),
  'medium': (font-size: 18, line-height: 1.4),
  'large': (font-size: 24, line-height: 1.25),
);

Vendor

We use Eric Meyer's reset to reset styling on base elements. Because each design is unique, do not use necolas/normalize.css or nathansmith/formalize since these are opinionated and add styles that you will end up overriding anyway.

Manifest

All files are imported into the single application.scss manifest. You may need additional manifest files for specific languages, or split large builds into multiple files for Internet Explorer.

@import 'utilities/mixins';
@import 'utilities/variables';

@import 'vendor/reset';
@import 'vendor/jquery.carousel';

@import 'base/fonts';
@import 'base/form';
@import 'base/layout';
@import 'base/typography';

@import 'components/icons';
@import 'components/masthead';
@import 'components/video_player';

@import 'layouts/base';
@import 'layouts/layout_6_6';
@import 'layouts/layout_12';

Linting

We use stylelint.io to ensure SCSS is written cleanly and consistently.

First, install the following stylelint plugin: * stylelint-selector-bem-pattern plugin

Then copy the .stylelintrc from the Tao repo into your project.

Linting configuration can be changed per project depending on specific requirements. General linting rules include:

  • Keeping all selectors BEM-y.
  • Each property/value pair on a new line.
  • Consistent whitespace between declarations.
  • Not nesting selectors more than five levels deep.

Do not disable rules inline for specific cases. While you own the code, the team owns the conventions, so please discuss with the team if you need to relax a specific rule project-wide.

(.stylelintrc)
{
  "plugins": [
    "stylelint-selector-bem-pattern"
  ],
  "ignoreFiles": [
    "public/assets/src/css/vendor/**"
  ],
  "rules": {
    "plugin/selector-bem-pattern": {
      "componentName": "(([a-z0-9]+(?!-$)-?)+)",
      "componentSelectors": {
        "initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
      },
      "ignoreSelectors": [
        ".*\\.no-js.*",
        ".*\\.lt-ie.*"
      ]
    },
    "color-no-invalid-hex": true,
    "color-hex-case": "lower",
    "declaration-colon-space-after": "always",
    "max-empty-lines": 1,
    "indentation": 2,
    "length-zero-no-unit": true,
    "number-leading-zero": "never",
    "string-quotes": "single",
    "value-no-vendor-prefix": true,
    "declaration-no-important": true,
    "declaration-block-no-duplicate-properties": true,
    "declaration-block-no-shorthand-property-overrides": true,
    "declaration-block-semicolon-newline-after": "always",
    "declaration-block-semicolon-space-before": "never",
    "declaration-block-trailing-semicolon": "always",
    "declaration-property-value-blacklist": {
      "/^border/": ["none"]
    },
    "block-closing-brace-newline-after": [
      "always",
      {
        "ignoreAtRules": [
          "if",
          "else",
          "else if"
        ]
      }
    ],
    "block-opening-brace-newline-after": "always",
    "block-opening-brace-space-before": "always",
    "selector-combinator-space-after": "always",
    "selector-combinator-space-before": "always",
    "selector-no-id": true,
    "selector-no-vendor-prefix": true,
    "selector-pseudo-element-colon-notation": "single",
    "selector-no-qualifying-type": true,
    "selector-list-comma-newline-after": "always",
    "selector-list-comma-space-before": "never",
    "rule-nested-empty-line-before": [
      "always",
      {
        "except": [
          "first-nested"
        ],
        "ignore": [
          "after-comment"
        ]
      }
    ],
    "rule-non-nested-empty-line-before": [
      "always",
      {
        "ignore": [
          "after-comment"
        ]
      }
    ],
    "media-feature-colon-space-after": "always",
    "media-feature-colon-space-before": "never",
    "media-feature-range-operator-space-after": "always",
    "media-feature-range-operator-space-before": "always",
    "at-rule-empty-line-before": [
      "always",
      {
        "except": [
          "blockless-group",
          "first-nested",
        ],
        "ignore": [
          "after-comment"
        ]
      }
    ],
    "media-query-parentheses-space-inside": "never",
    "max-nesting-depth": 5,
    "no-duplicate-selectors": true,
    "no-eol-whitespace": true,
    "no-missing-eof-newline": true
  }
}

Internet Explorer strategies

Check the project scope for which versions you need to support. See Browser support.

Do not use conditional comments to load per-device stylesheets but use the h5bp/html5-boilerplate convention of adding a class to the <html> element for each supported version. This allows you to keep IE-specific styles inline with each component:

.my-component {
  display: inline-block;

  .lt-ie9 & {
    display: inline;
  }
}