How we do CSS at Ghost

The CSS community is a wonderful thing to be part of. Everyone from GitHub to Lonely Planet has shared how they write CSS. Every time I read one of these posts, I learn something new; something I can use in my own day-to-day work.

The current state of CSS at Ghost is one of flux. We have rewritten Ghost's CSS a couple times to make it leaner and easier to learn. But there's more to the health of a code-base than it's quality. Comments and learnability are absolutely crucial to helping new contributors get involved and help Ghost grow more and more.

This post is a primer on how we do CSS at Ghost, and how parts will be done going forward. So, with all that said, let's dive in!

Contents

Introduction

As a rule, our CSS is purposefully kept as lean & clean as possible. We don't prefix properties, we don't nest deeply, and we don't use any pre-packaged libraries – if you exclude Normalize.

A quick note on browser support. We aim to target the last 2 versions of popular browsers. At the time of writing, that means IE 10 & IE 11. Other target browsers automatically update which makes them a non-issue.

Pre-Processor

We use Sass as our pre-processor. It is the most popular pre-processor around today and has the widest network of support and collective knowledge. We use Libsass via a node-sass Grunt task.

We use Libsass instead of Ruby-Sass, principally so we don't have a Ruby dependency. It's also much faster, taking an average of 3 seconds to generate 2 minified files and 2 source maps. One set for Ghost, the other for docs.

Prefixing is handled by Autoprefixer which is configured to only prefix the browsers we want to support. This means the only prefixed properties in the code are very specific to a particular browser; namely -webkit- stuff for a majority of mobile browsers.

Folder Architecture

The file structure we've adopted is simple but suits the reusability of various components very well.

core/client/assets/sass
|-- screen.scss
|-- vendor/
|-- modules/
|-- patterns/
|-- components/
|-- layouts/
  • screen.scss is where everything is @include'd, including Normalize from Bower
  • vendor/ is for CSS related to some JavaScript libraries
  • modules/ is for mixins, variables and utilities (such as icons)
  • patterns/ is for global styles, buttons, and forms
  • components/ is for groups of patterns with small bits of layout
  • layouts/ is where page layouts go and any page-specific changes to patterns and components

File Layout

Files have a strict layout, at least in terms of the way comments are written, blank lines added, and nesting is handled.

To help illustrate how, here's a full example showing every guideline and below is a list explaining each.

Note: This is not a real example, or well-crafted code. It's a guide to how code should be laid out and architected.

// ------------------------------------------------------------
// The Sass Example
//
// This is a brief example of how to structure a `.scss` file.
//
// * Define animations
// * Classed to use these animations
// ------------------------------------------------------------


//
// A wrapper for Element, extending %other-wrapper
// --------------------------------------------------

.element-wrapper {
    @extend &other-wrapper;

    padding: 15px 20px;
    border: 1px solid $lightbrown;

    @media (min-width: 801px) {
        margin: 10px;
        border-color: $midbrown;
    }

    // Variations
    &.darker {
        border-color: $midbrown;

        &.variation {
            padding-left: 10px;
            padding-right: 10px;
        }
    }

    .element-edit {
        border: 1px solid $lightbrown;
        border-radius: $rounded;

        @include icon($i-edit, 2rem, $lightbrown){
            transition: color linear 0.15s;
        };

        &:hover {
            border-color: $midbrown;

            &:before {
                color: $midbrown;
            }
        }
    }

}//.wide

//
// Another element
// --------------------------------------------------

.another-element {
    font-size: 1.6rem;
    line-height: 1.325;
    color: $darkgrey;
}
  • Every file has a title, and a slug it the file is specific to one place
  • Never style ID's and .js- classes
  • Use short-hand notation where possible
  • Blocks longer than 25 lines get a closing comment
  • Keep nesting to a minimum, and no deeper than 3 levels, including pseudo selectors (:nth-of-type and :after)
  • Group related properties together
  • Media queries always go after properties, in order of small to large width/height values
  • Don't be clever with selectors. Aim for readability over complexity

Naming Things

Variables

We've (rather unconventionally) decided not to use abstract variable names. As an example, we have $red instead of $error. It's much easier to understand and significantly lowers the barrier to entry.

Classes

We favour longer classes names and more of them over nesting selectors. So instead of a selector like .navigation ul li a, we would use a .navigation-link.

Files

If you stick to the rules explained in the folder architecture, the name for a new file should become fairly obvious. The name of the file should directly relate to its contents. If you find a single file has 2 very separate chunks of code in it, they should probably be abstract out into separate files.

Media Queries

In very rare cases we detect the type of device (desktop or touch device), but everything else is based on viewport width and sometimes feature availability. As such, there's plenty of media queries dotted around the code.

So far, we've chosen not to use variables in media queries in lieu of more readability. However - thinking forward - there are several places that would benefit from having a variable in the MQ.

Comments

Code Tells You How, Comments Tell You Why Jeff Atwood - Coding Horror

There will be times when the code has to be complex, be it the necessary implementation, sheer complexity of properties, or inherited values.

If any CSS you write is not immediately obvious in context, it should be commented - not explaining how it works, but why its there in the first place.

.sample-component {
    @extend .other-component;

    transform: translate(0,0,0); // Resets `translate` inherited from `.other-component`
}

Linting

Currently, Ghost's Sass is not linted. That has to change. Linting the JavaScript of Ghost has helped keep the quality high before it even gets committed to the code base. Doing the same with CSS will ensure the code is consistent throughout.

Things that should be linted, include:

  • Levels of nesting & indentation
  • Flag uses of ID selectors and classes beginning with .js-
  • Spacing after selectors, properties, and values
  • Use of single & double quotes
  • Formatting and order of properties
  • Selector formatting, preferring all lowercase with hyphens
  • Null values with a unit (e.g. 0px should be 0 or none)
  • Use of colour names, preferring $red over red
  • Duplicate properties
  • Duplicate selectors on the same level

At the time of writing, there are no libraries that will lint .scss files that dont require a Ruby dependency, but this should serve as a list of requirements for tools that crop up in the mean time.

Documentation

We do have some documentation, but it's far from ideal. It's currently a Jekyll site deep in the folder structure core/client/docs. Right now, it houses examples of reusable components, or patterns in our terms.

I'm not convinced that having it auto-regenerate is the greatest of ideas, but it certainly needs some love.

You Can Help

We think (once all this is implemented), Ghost's CSS will be really easy to work on. However, there is always room to improve. If you want to help out or make the CSS even better, there's plenty of ways to talk to the awesome contributors.