How Animoto writes CSS March 31, 2015

We started caring about authoring great CSS much later than we’re proud to admit. It’s a language that has a low barrier to entry and feels simple, but even the best front-end engineers will write it poorly when nobody’s looking. I’ve done it. You’ve done it. Unfortunately, that means CSS gets out of hand very quickly—bloated files, ridiculous selectors, inconsistent styles—and you wind up with something like this screenshot of actual code from our site (pre-optimization, of course):

Creation Flow CSS bloat

Writing like that doesn’t scale. Writing like that makes it impossible to learn. We decided to fix it.

We wanted to create a system that would:

  • Be easy to learn.
  • Standardize our syntax.
  • Make it difficult to cause unintended changes.
  • Push designers and developers toward reusable components.

We also had a a few assumptions about how we’d be making that system:

  • We would adopt SCSS.
  • We would use Autoprefixer.
  • We would not convert pages in-place (rewrites would have to be tied to existing product initiatives / redesigns).

Many blogs in the past year have featured folks doing 1-to-1 conversions of their site to SCSS, replete with selector counts and performance stats. The size of our codebase and the rate we generate new style code meant that there was definitely no way we’d be able to do that, and we would lose the ability to use trial-and-error as a learning tool (if we did it all at once, we’d have to know at the outset that we’re doing everything perfectly. That wasn’t an assumption we wanted to make). Beyond that, the actual cost of pulling the entire front-end team off of their existing initiatives for an indeterminable length of time seemed like a tough sell.

Instead, we got buy-in from our PMs and the design team (they wanted to refresh our brand anyway, which meant we had a perfect opportunity), and we started building time into our roadmapped projects for creating the skeleton of our SCSS system.

After reviewing every CSS framework we could hunt down (plus all the amazing articles posted last year about how other companies approach CSS), and realized that we needed our own rules—not because we think we’re doing it the one true way, but because requirements vary from project to project and team to team. Continue reading with that knowledge: that our rules are for Animoto, and they’re not intended to be a drop-in solution for all projects. We’re hoping that, by contributing to the sharing culture growing around how companies write styles, we can help get you (and your team) thinking more about how you write CSS.


File organization

Previously, we had styles divided into folders named “base”, “layout”, “pages”, “ui”, and “vendor”, which was a framework we didn’t want to stray too far from, because we knew we’d have both systems living at the same time. We created an SCSS-only directory with the following structure:

Base

Base contains our reset or normalize–style rules, such as link behavior and typography, as well as our grid system and a file of helper classes. In Base and Base alone, it’s alright to use element selectors.

Components

Components are the building blocks of our site, such as our buttons, loading spinner, or color picker. Before we write new components, they have to pass some basic smell tests:

  • Does this component have a unique purpose? If not, we sync with a designer and find a way to combine them or create a hard rule for when we’d use one over the other.
  • Can this be put into different designs without writing new code? If not, find a way to make it portable. It doesn’t have to look exactly the same—proportions may change, for example—but it should be rethought so it has drop-in reusability.
  • Is this further reducible? If so, make it simpler. Components should be fully-baked, with nothing that can be added or removed.

Modules

Modules, on the other hand, are collections of components and other modules.

When we’re creating components and modules, we consider whether or not it actually makes sense to define margins, size, and float within their file directly—because those tend to rely more on the item’s interplay with other elements, we usually keep those properties in page-specific CSS, or as part of the module they’re being nested within.

As such, modules are often mostly just a series of box model and positioning declarations that define how typographic elements and components work together.

Pages

Pages are essentially a special type of module in our system. Just like modules, they mostly define how to lay out their child elements. They’ve also got the special task of defining background images, which we basically never include in a module. And, because defining the location and behavior of a background image (in the module) is a different concern than actually specifying the image (in the page), we add a new class to the element instead of styling directly on the CSS hooks that the module needs.

Our _homepage.scss file, for example, looks like this (it’s 47 lines total):

.homepage {
.cmo-bg-image {
  background-image: url('/images/home/hp-cmo-feature-mobile-1x.jpg');
}
.photo-bg-image {
  background-image: url('/images/home/hp-pho-feature-mobile-1x.jpg');
}
.smb-bg-image {
  background-image: url('/images/home/hp-smb-feature-mobile-1x.jpg');
}

…

Utils

Utils is where we keep our in-house Sass magic. Here, we define global variables, mixins, extends, and functions. Basically our only rule is that nothing in /utils/ outputs any CSS directly (which is why we include our helper classes in Base). We make use of Sass for loads of custom behavior to make our lives easier—but more importantly, because they remind us to follow our own rules.

An extreme example is _rhythm.scss, which just multiplies any value by 8px. Anywhere we see a distance specified without using rhythm(X), it’s a red flag that someone’s using magic numbers.

$baseline: 8px;

@function rhythm($n) {
  @return $n * $baseline;
}

Naming conventions

“If you can’t make a good name for it, don’t make it at all.” – My coworker Vanessa Pyne, totally nailing our CSS philosophy.

Inside of each of our page, module, or component files, there is one top-level class that matches the filename: _homepage.scss starts with .homepage, _features_callout.scss starts with .features-callout. (This also means that names for pages, modules, and components must be unique across those three directories).

We loved the way BEM and OOCSS prefix a component or module’s children with their parent’s name, because it drives home that the children only exist in the context of their parent and, of course, because it helps prevent collisions. But we weren’t sold on including the parent’s full name as a prefix on each class. There are arguments to be made that gzip eliminates the problem, or that it’s incredibly descriptive, but honestly, we just didn’t want to sign up for that much typing.

Instead, we use the parent’s initials as a prefix:

.features-callout {
  padding: 60px 0;

  text-align: center;

  .fc-header {
    margin-bottom: rhythm(6);
  }
  .fc-list {
    list-style: none;
  }
  .fc-list-item {
    margin: 10px 0;
  }
  .fc-copy {
    margin-top: rhythm(2);
    margin-bottom: rhythm(6);
  }
}

It still prevents collisions, but we can see that it’s not 100% future-proof (while top-level classes will always be unique, their initials might not) and we’re keeping an eye on it as the system scales up. Inherent in this approach is a ban on element selectors within modules and components, which is a great performance and maintenance boost as well.

Using a prefixed system also allows us to keep our selectors very flat. We use the SCSS-Lint default nesting depth (3), but we really do our best to keep it down to just two classes. In the example above, for instance, .fc-list-item is a child of .fc-list in the DOM, but we don’t nest it in our SCSS to reflect that. Aside from the selector functioning faster (and smaller outputted CSS), it unties our CSS from a specific implementation in HTML.

We also have a list of generic classes that nobody’s allowed to use, such as .primary, .module, .page, .wrapper, .nav, and .new. Half because they’re most likely to cause collisions; half because they’re usually a sign you’re being lazy.

To keep Javascript and CSS separate, we use a .js- prefix on any class or ID needed for Javascript and keep those classes out of our CSS entirely. (Side note: if you start using .js-, let your test team know to use those for hooks instead of styling classes). We have one point where the two languages touch: .is-. That lets us know that it’s a class that will be toggled on and off with Javascript, such as .is-hidden or .is-active.


Rule ordering

We order properties by type, by importance, by CSS convention, and finally, in alphabetical order. It’s not nearly as complicated as that makes it sound, though. Properties are split into groups, and groups are separated by an empty line (even if there’s just one rule in every group and it looks ridiculous. Yes, even then).

.some-cool-class {
  /* Scoped variables */
  $someHeight: 86px;

  /* Extends, followed by imports */
  /* (Sass hoists @extends up, so putting it first reflects the output) */
  @extend %other-cool-class;
  @include border;

  /* For pseudo elements */
  content: "";

  /* Positioning */
  position: relative;
  float: right;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;

  /* Most of the box model. `border` would go between */
  /* padding and margin (we're working from inside the element outward here),*/
  /* but border should be added using our border mixin (@include border). */
  display: block;
  visibility: hidden;
  width: 100px;
  height: $someHeight;
  overflow-x: hidden;
  padding: 8px;
  outline: 1px solid;
  border: 2px solid; // use @include instead, but would be after outline
  margin: 16px;

  /* Background stuff, in alphabetical order */
  background-image: url("../visuals/icons/position_top_n.svg");
  background-position: top left;
  background-repeat: no-repeat;
  background-size: 100px 100px;

  /* Typographic rules, in alphabetical order */
  line-height: $someHeight;
  text-align: center;
  vertical-align: middle;

  /* Animation rules, in alphabetical order */
  transition: background 0.4s;

  /* Other rules, in alphabetical order */
  cursor: pointer;
  opacity: 1;

  /* Modifiers / states (in the following order: :hover, :active/.is-selected, :focus, .is-disabled) */
  &:hover,
  &:active {
    ...
  }
  &:hover {
    ...
  }
  &.is-disabled {
    ...
  }

  /* Children */
  .scc-child-element {
    ...
  }
  .scss-grandchild-element {
    ...
  }

  /* Breakpoints */
  @include breakpoint($breakpoint-medium $breakpoint-large) {
    width: 100%;
  }
}

:after

That’s Animoto CSS 101. (Don’t worry, blog posts about how we review and enforce this system are forthcoming).

We’ve been using this system for about five months now (with a few refinements along the way), and it’s been working very well for us. Judged against our own goals, we’ve done a great job. It’s very easy to wrap your head around the nomenclature, and we’re starting to really nail how to design using components and modules. Files have started to look (and behave) in consistent ways, and we’re no longer feeling the shock of a stray ID in some far off file wrecking our styles. We’ve onboarded people, and for the first time, they haven’t been crippled by will-this-break-something-itis.

If you’re going to take one thing away from this, it’s that you should treat yourself like the consumer. Do what you’d do with any other deliverable: research, discuss, and refine.

Good CSS styles your site. Great CSS does that and keeps your team sane.

Brendan LaCroix
Author
Brendan LaCroix