Skip to content

Instantly share code, notes, and snippets.

@czottmann
Forked from bobbygrace/trello-css-guide.md
Last active January 14, 2016 12:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save czottmann/eb3d1a24adc26969d41d to your computer and use it in GitHub Desktop.
Save czottmann/eb3d1a24adc26969d41d to your computer and use it in GitHub Desktop.
Scompler CSS Guide

Scompler CSS Guide

"I perfectly understand our CSS. I never have any issues with cascading rules. I never have to use !important or inline styles. Even though somebody else wrote this bit of CSS, I know exactly how it works and how to extend it. Fixes are easy! I have a hard time breaking our CSS. I know exactly where to put new CSS. We use all of our CSS and it's pretty small overall. When I delete a template, I know the exact corresponding CSS file and I can delete it all at once. Nothing gets left behind."

You often hear updog saying stuff like this. Who's updog? Not much, who is up with you?

This is where any fun you might have been having ends. Now it's time to get serious and talk about rules.

Writing CSS is hard. Even if you know all the intricacies of position and float and overflow and z-index, it's easy to end up with spaghetti code where you need inline styles, !important rules, unused cruft, and general confusion. This guide provides some architecture for writing CSS so it stays clean and maintainable for generations to come.

There are eight fascinating parts.

  1. Tools
  2. Components
  3. Javascript
  4. Mixins
  5. Utilities
  6. File Structure
  7. Style
  8. Miscellany

1. Tools

Use only imports, variables, and mixins (and only for vender-prefixed features) from CSS preprocessors.

To keep our CSS readable, we try and keep our CSS very vanilla. We use SASS, but only use imports, data-uri, variables, and some mixins. We use imports so that variables and mixins are available everywhere and it all outputs to a single file. We occasionally use nesting, but only for very shallow things like &:hover or modifiers (more on that later). We don't use more complex functions like guards and loops.

If you follow the rest of the guide, you shouldn't need the complex functions in preprocessors. Have I piqued your interest? Read on…

2. Components

Use the .Component-descendant-descendant pattern for components.

Components help encapsulate your CSS and prevent run-away cascading styles and keep things readable and maintainable. Central to componentizing CSS is namespacing. Instead of using descendant selectors, like .global-header img { … }, you'll create a new hyphen-separated class for the descendant element, like .GlobalHeader-image { … }.

Here's an example with descendant selectors:

.global-header {
  background: hsl(202, 70%, 90%);
  color: hsl(202, 0%, 100%);
  height: 40px;
  padding: 10px;
}

  .global-header .logo {
    float: left;
  }

    .global-header .logo img {
      height: 40px;
      width: 200px;
    }

  .global-header .nav {
    float: right;
  }

    .global-header .nav .item {
      background: hsl(0, 0%, 90%);
      border-radius: 3px;
      display: block;
      float: left;
      -webkit-transition: background 100ms;
      transition: background 100ms;
    }

    .global-header .nav .item:hover {
      background: hsl(0, 0%, 80%);
    }

And here's the same example with namespacing:

.GlobalHeader {
  background: hsl(202, 70%, 90%);
  color: hsl(202, 0%, 100%);
  height: 40px;
  padding: 10px;
}

  .GlobalHeader-logo {
    float: left;
  }

    .GlobalHeader-logo-image {
      background: url("logo.png");
      height: 40px;
      width: 200px;
    }

  .GlobalHeader-nav {
    float: right;
  }

    .GlobalHeader-nav-item {
      background: hsl(0, 0%, 90%);
      border-radius: 3px;
      display: block;
      float: left;
      -webkit-transition: background 100ms;
      transition: background 100ms;
    }

    .GlobalHeader-nav-item:hover {
      background: hsl(0, 0%, 80%);
    }

Namespacing keeps specificity low, which leads to fewer inline styles, !important declarations, and makes things more maintainable over time.

Make sure every selector is a class. There should be no reason to use id or element selectors. No underscores, component is PascalCase, everything else should be lowercase.

Components make it easy to see relationships between classes. You just need to look at the name. You should still indent descendant classes so their relationship is even more obvious and it's easier to scan the file. Stateful things like :hover should be on the same level.

Modifiers

Use the .Component-descendant.mod-modifier pattern for modifier classes.

Let's say you want to use a component, but style it in a special way. We run into a problem with namespacing because the class needs to be a sibling, not a child. Naming the selector .Component-descendant-modifier means the modifier could be easily confused for a descendant. To denote that a class is a modifier, use a .mod-modifier class.

For example, we want to specially style our sign up button among the header buttons. We'll add .GlobalHeader-nav-item.mod-sign-up, which looks like this:

<!-- HTML -->

<a class="GlobalHeader-nav-item mod-sign-up">
  Sign Up
</a>
// global-header.scss

.GlobalHeader-nav-item {
  -webkit-transition: background 100ms;
  background: hsl(0, 0%, 90%);
  border-radius: 3px;
  display: block;
  float: left;
  transition: background 100ms;

  &.mod-sign-up {
    background: hsl(120, 70%, 40%);
    color: #fff;
  }
}

We inherit all the GlobalHeader-nav-item styles and modify it with .mod-sign-up. This breaks our namespace convention and increases the specificity, but that's exactly what we want. This means we don't have to worry about the order in the file. For the sake of clarity, put it inside the part of the component it modifies.

You should never write a bare .mod- class. It should always be tied to a part of a component. .GlobalHeader-button.mod-sign-up { background: green; } is good, but .mod-sign-up { background: green; } is bad. We could be using .mod-sign-up in another component and we wouldn't want to override it.

Modifiers should be descriptive but not too specific. Good: .mod-sign-up, .mod-standout. Bad: .mod-red-background, .mod-italic, .mod-bigger-than-usual.

You'll often want to overwrite a descendant of the modified selector. Do that like so:

.GlobalHeader-nav-item {
  ...

  &.mod-sign-up {
    background: hsl(120, 70%, 40%);
    color: #fff;

    .GlobalHeader-nav-item-text {
      font-weight: bold;
    }
  }
}

Generally, we try and avoid nesting because it results in runaway rules that are impossible to read. This is an exception.

State

Use the .Component-descendant.is-state pattern for state. Manipulate .is- classes in Javascript (but not presentation classes).

State classes show that something is enabled, expanded, hidden, or what have you. A state is something fleeting, temporary, like .is-loading. For these classes, we'll use a new .Component-descendant.is-state pattern.

Example: Let's say that when you click the logo, it goes back to your home page. But because it's a single page app, it needs to load things. You want your logo to do a loading animation.

You'll use a .GlobalHeader-logo-image.is-loading rule. That looks like this:

.GlobalHeader-logo-image {
  background: url("logo.png");
  height: 40px;
  width: 200px;

  &.is-loading {
    background: url("logo-loading.gif");
  }
}

Javascript defines the state of the application, so we'll use Javascript to toggle the state classes. The .Component.is-state pattern decouples state and presentation concerns so we can add state classes without needing to know about the presentation class. A developer can just say to the designer, "This element has an .is-loading class. You can style it however you want.". If the state class were something like .GlobalHeader-logo-image--is-loading, the developer would have to know a lot about the presentation and it would be harder to update in the future.

Like modifiers, it's possible that the same state class will be used on different components. You don't want to override or inherit styles, so this is important: Every component define its own styles for the state. They should never be defined on their own. Meaning you should see .GlobalHeader.is-hidden { display: none; }, but never .is-hidden { display: none; } (as tempting as that may be). .is-hidden could conceivably mean different things in different components.

State classes should be descriptive but not too specific. Good: .is-hidden, .is-loading. Bad: .is-float-right, .is-red, .is-underlined.

We also don't indent state classes. Again, that's only for descendants. For the sake of clarity, put them inside the relevant part of the component, after the modifiers.

Keeping It Encapsulated

Components can control a large part of the layout or just a button. In your templates, you'll likely end up with parts of one component inside another component, like a .Button inside a .MemberList. We need to change the button's size and positioning to fit the list.

This is tricky. Components shouldn't know anything about each other. If the smaller button can be reused in multiple places, add a modifier in the button component (like .Button.mod-small) and use it in .MemberList. Do the positioning with a member list component with a descendant, since that's specific to the member list and not the button.

Here's an example:

<!-- HTML -->

<div class="MemberList">
  <div class="MemberList-item">
    <p class="MemberList-item-name">Gumby</p>
    <div class="MemberList-item-action">
      <a href="#" class="Button mod-small">Add</a>
    </div>
  </div>
</div>
// button.scss

.Button {
  background: #fff;
  border: 1ps solid #999;
  padding: 8px 12px;

  &.mod-small {
    padding: 6px 10px;
  }
}


// member-list.scss

.MemberList {
  padding: 20px;
}

  .MemberList-item {
    margin: 10px 0;
  }

    .MemberList-item-name {
      font-weight: bold;
      margin: 0;
    }

    .MemberList-item-action {
      float: right;
    }

A bad thing to do would be this:

<!-- HTML -->

<div class="MemberList">
  <div class="MemberList-item">
    <p class="MemberList-item-name">Pat</p>
    <a href="#" class="MemberList-item-button Button">Add</a>
  </div>
</div>
// member-list.scss

.MemberList-item-button {
  float: right;
  padding: 6px 10px;
}

In the bad example, .MemberList-item-button overrides styles specific to the button component. It assumes things about button that it shouldn't have to know anything about. It also prevents us from reusing the small button style and makes it hard to clean or change up later if needed.

You should end up with a lot of components. Always be asking yourself if everything inside a component is absolutely related and can't be broken down into more components. If you start to have a lot of modifiers and descendants, it might be time to break it up.

3. Javascript

Separate style and behavior concerns by using .js- prefixed classes for behavior.

For example:

<!-- HTML -->

<div class="ContentNav">
  <a href="#" class="ContentNav-button js-open-content-menu">
    Menu
  </a>
</div>
// Javascript (with jQuery)

$('.js-open-content-menu').on('click', function(evt) {
  openMenu();
});

Why do we want to do this? The .js-* class makes it clear to the next person changing this template that it is being used for some Javascript event and should be approached with caution. .js-* classes are used by older, non-React JS code.

Be sure to use a descriptive class name. The intent of .js-open-content-menu is more clear than .js-menu. A more descriptive class is less likely to conflict with other classes and it's lots easier to search for. The class should almost always include a verb since it's tied to an action.

.js-* classes should never appear in your stylesheets. They are for Javascript only. Inversely, there is never a reason to see presentation classes like .header-nav-button in Javascript. You will see state classes like .is-state in your Javascript and your stylesheets as .Component.is-state.

4. Mixins

SASS mixins are shared styles that are used in more than one component. Mixins should not be standalone classes or used in markup. They should be single level and contain no nesting. Mixins make things complicated fast, so use sparingly.

When using a mixin, it should include the parenthesis to make it more obvious that it's a mixin. Example usage:

// mixins.scss

@mixin clearfix {
  &:after {
    content: "";
    display: table;
    clear: both;
  }
}


// component.scss
.Component-descendent {
  @include clearfix;
}

5. Utilities

Utility classes are classes like .u-float-left, .u-spacing-top, .u-bold-text etc.

We don't use utility classes at all.

We don't need something like .u-float-left { float: left; } where including float: left; in the component is just as easy and more visible.

6. File Structure

The file will look something like this:

@charset "UTF-8"

@import "normalize.css"

// Variables
@import "media-queries.less"
@import "colors.less"
@import "other-variables-like-fonts.less"

// Mixins
@import "mixins.less"

// Utils
@import "utils.less"

// Components
@import "component-1.less"
@import "component-2.less"
@import "component-3.less"
@import "component-4.less" // and so forth

Include normalize.css at the top of the file. It standardizes CSS defaults across browsers. You should use it in all projects. Then include variables, mixins, and utils (respectively).

Then include the components. Each component should have its own file and include all the necessary modifiers, states, and media queries. If we write components correctly, the order should not matter.

This should output a single app.css file (or something similarly named).

7. Style

Even following the above guidelines, it's still possible to write CSS in a ton of different ways. Writing our CSS in a consistent way makes it more readable for everyone. Take this bit of CSS:

.GlobalHeader-nav-item {
  -webkit-transition: background 100ms;
  background: hsl(0, 0%, 90%);
  border-radius: 3px;
  display: block;
  float: left;
  padding: 8px 12px;
  transition: background 100ms;
}

It sticks to these style rules:

  • Use a new line for every selector and every declaration.
  • Use two new lines between rules.
  • Add a single space between the property and value, for example prop: value; and not prop:value;.
  • Alphabetize declarations.
  • Use 2 spaces to indent, not 4 spaces and not tabs.
  • No underscores or camelCase for selectors. PascalCase for the component name, hyphens and lowercase for the rest.
  • Use shorthand when appropriate, like padding: 15px 0; and not padding: 15px 0px 15px 0px;.
  • When using vendor prefixed features, put the standard declaration last. For example: -webkit-transition: all 100ms; transition: all 100ms;. (Note: Browsers will optimize the standard declaration, but continue to keep the old one around for compatibility. Putting the standard declaration after the vendor one means it will get used and you get the most optimized version.)
  • Prefer rgba(a) over hex.

8. Miscellany

You might get the impression from this guide that our CSS is in great shape. That is not the case. While in the beginning we've stuck to .js-* classes and often use namespaced-component-looking classes, there is a mishmash of styles and patterns throughout. That's okay. Going forward, you should rewrite sections according to these rules. For example, since we started rewriting our old jQuery-based JS as React components, you'll see less and less .js-* classes around.

Leave the place nicer than you found it.

Some additional things to keep in mind:

  • Comments rarely hurt. If you find an answer on Stack Overflow or in a blog post, add the link to a comment so future people know what's up. It's good to explain the purpose of the file in a comment at the top.
  • In your markup, order classes like so <div class="Component mod state js" />.
  • You can embed common images and files under 10kb using datauris. This saves a request, but adds to the CSS size, so only use it on extremely common things like the logo.
  • Avoid body classes. There is rarely a need for them. Stick to modifiers within your component.
  • If possible, explicitly write out class names in selectors. Don't concatenate strings or use preprocessor trickery to build a class name. We want to be able to search for class names and that makes it impossible.
  • If you are worried about long selector names making our CSS huge, don't be. Compression makes this a moot point.

Some additional reading on CSS architecture around the web:

Performance

Performance probably deserves it's own guide, but I'll talk about two big concepts: selector performance and layouts/paints.

Selector performance seems to matters less and less these days, but can be a problem in a complex, single-page app with thousands of DOM elements (like Trello). The CSS Tricks article about selector performance should help explain the important concept of the key selector. Seemingly specific rules like .Component-descendant-descendant div are actual quite expensive in complex apps because rules are read from right to left. It needs to look up all the divs first (which could be thousands) then go up the DOM from there.

Juriy Zaytsev's post on CSS profiling profiles browsers on selector matching, layouts, paints, and parsing times of a complex app. It confirms the theory that highly specific selectors are bad for big apps. Harry Roberts of CSS Wizardry also wrote about CSS selector performance.

You shouldn't have to worry about selector performance if you use components correctly. It will keep specificity about as low as it gets.

Layouts and paints can cause lots of performance damage. Be cautious with CSS3 features like text-shadow, box-shadow, border-radius, and animations, especially when used together. We wrote a big blog post about performance back in January 2014. Much of this was due to layout thrashing caused by Javascript, but we cut out some heavy styles like borders, gradients, and shadows, which helped a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment