@mdo

GitHub's CSS

July 23, 2014

I’m always interested in the development details of other products, particularly their styleguides and approach to CSS. Given my penchant for the otherwise inane CSS details, I decided to write a bit about GitHub’s CSS.

Contents

Given the length, here’s a helpful table of contents.

Quick facts

A survey of our current state of CSS shows:

  • The preprocessor of choice is SCSS.
  • We have well over 100 individual source stylesheets that we compile before serving it up in production.
  • That compiled CSS is served via two separate stylesheets to avoid the IE<10 selector limit.
  • Those two stylesheets weigh in at around 90kb total.
  • There’s no particular “CSS architecture” in place.
  • Pixels are the unit of choice, although we do have some ems lying around.
  • Added 7/24/14: We use Normalize.css and a mix of our own reset styles.

Preprocessor

As mentioned above, we use SCSS. This was a choice made much before my time and I’m a-okay with it (despite Bootstrap currently being written in LESS). Our SCSS is compiled by the Rails asset pipeline with some help from Sprockets (for the time being) for including the files. Read about that a bit farther down.

What about LESS, or Stylus, or …? I don’t think LESS was ever a choice for GitHub, but I can’t really speak to that. We probably wouldn’t switch at this point either as there is no clear benefit.

Why a preprocessor? Our internal framework includes a relatively small set of variables (like font stacks and brand colors) and mixins (mostly for vendor prefixed properties) that make writing code faster and easier.

We don’t currently use Autoprefixer, but we really should as it would basically negate all our existing mixins. Hopefully we’ll implement it soon.

We also don’t currently use source maps, but that’ll be changing soon. (If you don’t know, source maps allow you to see in the Inspector what source SCSS file a particular set of styles are coming from rather than a compiled and minified stylesheet. They’re awesome.)

Added 7/24/14: Other than that, our use of the tools SCSS provides is quite limited. We use color variables, mixins, color functions, math functions, and nesting. We don’t need to iterate over classes, set global options, or anything else really. I enjoy the simplicity this affords.

Architecture

Common CSS architectures include BEM and OOCSS. We lean towards OOCSS, but we have no holistic approach. We do tend to write all our new stuff with a nebulous but basic approach:

  • Aim for classes over everything else in selectors
  • Avoid unnecessary nesting
  • Use (single) dashes in class names
  • Keep things as short as possible without adding confusion

I’ll write more about my preferred CSS architecture in another post. For now, the above sums up GitHub’s approach, which while not perfect, serves its purpose well enough.

Linting

We didn’t start linting our SCSS until several weeks ago. We had common enough conventions, but everyone’s style and formatting was somewhat unique. Today, every CI build includes basic SCSS linting and will fail if:

  • You have a class in your CSS but not anywhere in your app/views/ templates.
  • You are using the same selector multiple times (as they should almost always be combined).
  • The general formatting rules (nesting limits, line breaks between rulesets, lack of space after :s, etc) are broken.

All in all, these few rules keep our stuff decently tidy. It doesn’t account for discrepancies in commenting styles or general architecture, but that’s the stuff teams need to address themselves with real documentation. It’s something everyone here is open to improving and iterating on.

Two bundles

GitHub.com has two CSS bundles, github and github2. The split was added years ago to solve the problem of Internet Explorer’s 4,095 selectors per file limit. This applies to IE9 and down, and since GitHub requires a minimum of IE9, our split bundle approach must stay.

This is because, as of today, GitHub has around 7,000 selectors across those two files. How’s that compare to other sites?

  • Bootstrap v3.2.0 has just under 1,900 selectors
  • Twitter has just under 8,900 selectors
  • NY Times has just under 2,100 selectors
  • SoundCloud has under 1,100 selectors (Edit: Previously I reported this at 7,400, which is the old SoundCloud)

These numbers were gathered using cssstats.com. It’s an awesome little tool that looks at your CSS in ways most folks, including myself, usually don’t. We also have graphs for this internally at GitHub and usually use those for our own purposes.

Included via Sprockets

GitHub’s CSS, and JavaScript, is bundled via Sprockets and require. We maintain our two CSS bundles with separate subdirectories within the app/assets/stylesheets directory. Here’s how that looks:

/*
 = require primer/basecoat/normalize
 = require primer/basecoat/base
 = require primer/basecoat/forms
 = require primer/basecoat/type
 = require primer/basecoat/utility
 = require_directory ./shared
 = require_directory ./_plugins
 = require_directory ./graphs
 = require primer-user-content/components/markdown
 = require primer-user-content/components/syntax-pygments
 = require primer/components/buttons
 = require primer/components/navigation
 = require primer/components/behavior
 = require primer/components/alerts
 = require primer/components/tooltips
 = require primer/components/counter
 = require primer-select-menu
 = require octicons

 = require_directory .
*/

We include our dependencies—Primer is our internal framework—and then load the entire directory’s SCSS files in whatever order Sprockets decides to include them (I believe it’s alphabetical). The ease in which we can bundle our stylesheets—by simply writing require_directory .—is awesome, but it also kind of sucks.

The order in which styles get applied matters. It really shouldn’t, but in every design system there are rules and a basic hierarchy of styling. With the Sprockets approach, we sometimes run into specificity problems. This happens because new files can be added to either bundle at any time. Depending on their file name, they appear in different spots in the compiled CSS.

Additionally, using Sprockets means that your SCSS files don’t have immediate and automatic access to your global variables and mixins. That means you have to @import them explicitly at the top of any file that references a variable or mixin.

Given the repetition and occasional specificity headache, we have a pull request open to switch to explicit @imports. The added benefit is clearer insight into our CSS and easier willy-nilly experimentation with our aforementioned bundles. The downside is having to maintain that list, but I like to think of this as an added control to the overall system.

Performance

Added 7/24/14: Bundle sizes and selector count graphs.

Internally we use tons of graphs to monitor how the site and API are doing. We also track a few interesting frontend stats. For example, here’s the size of our two CSS bundles for the last three months:

GitHub bundle sizes

Similarly, here’s the number of selectors over the last month on our blob (or file) pages. Clearly we still have some work to do there to get those element tag selectors down.

GitHub selector count

Because we regularly deploy updated CSS, and we deploy dozens of times each day, we’re constantly busting the caches on our decently large CSS files. We haven’t done much in the way of optimizing these file sizes or limiting the cache busting, but we are starting to look into that more closely. It’d be awesome to have a core bundle that hopefully changes very infrequently and secondary bundle that can be more volatile.

Speaking of which, at Twitter we had (I’m unsure if they still have this) two bundles, core and more. core was all the styles needed to keep the time to first tweet number as low as possible. Everything else was in more. Given GitHub’s love of all things fast—here, it’s not shipped until it’s fast—it’s something we plan on looking into. Currently our bundle split is arbitrary.

Generally, selector performance isn’t something we concern ourselves with. We’re aware of bad practices—over-nesting, IDs, elements, etc—but we don’t try to over optimize. The one exception has been diff pages. Due to the extensive markup required in rendering a diff, we avoid attribute selectors like [class^="octicon"]. When used too often, those attribute selectors can (and have) crashed browsers.

For the curious, Jon Rohan, a fellow GitHub design-gineer, has a great talk about GitHub’s CSS performance that covers these problems.

Documentation

GitHub styleguide

Speaking of which, we do an okay job of this, but we’re working on making improvements. We have a publicly accessible CSS styleguide and all our general rules for writing CSS live there, as well as examples of most components. It’s built on KSS, a styleguide generator of sorts, and lives right in the github/github code base.

It’s not perfect, but it helps folks find and use things. It’s also a great way to show new folks how we do things to get them up to speed faster (like me when I joined nearly two years ago).

Primer

Primer

I alluded to it earlier, but for folks who don’t know, Primer is our internal framework for common styles and components across our public and internal apps. It includes:

  • Normalize
  • Global styles for box-sizing, typography, links, etc
  • Navigation
  • Forms
  • Grid system
  • Markdown styles
  • Custom select menu

We use it on GitHub.com, Gist, and several internal apps. As a rule of thumb, if it can or should be used in another app, we consider putting it in Primer.

Most of Primer’s components are documented in our styleguide (e.g., navigation, tooltips, etc). However, we’ve recently been going to town on updating and improving Primer, so lots of things are in flux there. We’ll be updating that soon to include all the things.

And for those about to ask, I’d love to open source parts of Primer, but there’s not much happening on that front in the near future. I’m hopeful though.

Refactoring

We have our fair share of legacy code at GitHub, and that includes our CSS. Unlike a public open source project with strict versioning rules, we burn shit down regularly if it suites us. Finding things to remove tends to happen one of two ways:

  • Manually finding things that look alike but actually have different HTML and CSS, then combining them.
  • Running a script that greps for a class in our CSS to see if it exists in our views. (We’ve recently made this part of our CI tests, so we constantly see it now.)

The actual process of refactoring CSS is probably not unique to GitHub. We find shit to remove, burn it down, open a PR, ping the CSS team, and ship it as fast as we can. As to who removes code, anyone can do it. We have tons of great folks adding to GitHub, but we also have just as many nerds looking at what we can remove.