SASS Antipatterns: Scoping to a Page

This is a story about a Rails project and how its SASS became difficult to manage.

The designers were still getting used to Rails’ structure and sometimes had trouble figuring out which view was being rendered, so we added something like this to the application layout:

<body id="<%= params[:controller].gsub('/', '-') %>-<%= params[:action] %>">

When you saw id="settings-profile" you would know the server was rendering the SettingsController’s profile template, and have a rough idea of where to look to modify the output.

This worked! And once we had it we realised that we could use the #settings-profile selector to restrict styles to that one page - useful if you need to tweak something in a single situation

This was a bad idea.

Proliferation of selectors

As we added features to the application, components that were part of one page would be included in other pages as well. An element with the class avatar might start on the profile settings page, and then later be added to the rest of the settings pages.

So we would end up with something like this:

#settings-profile, #settings-contact, #settings-applications, ... {
  .avatar {
    ...
  }

  ...
}

Every page id added to the list produced n extra selectors in the generated CSS.

This bloated the CSS, but the bigger problem was that it led us straight into Internet Explorer’s 4095-rule limit. If you’re not already aware of it, it’s very puzzling to discover that IE is refusing to apply the last half of your CSS. The only fix is to reduce the number of selectors you’re using, or to split your CSS up.

Specificity wars

Now that your selectors have an id in them, CSS’ specificity rules make it difficult to override them. You can’t customize a component with a generic class selector; you either need to add !important to its properties, or make the selector more specific by adding an id - probably the page id you scoped the original selector to.

This might cause problems later on, but you can worry about that when it happens, right?

The slippery slope

Once you have some major components scoped to the page id, the logic of specificity slowly sucks in more and more of your CSS. Your styles are now organized by the pages they appear on, instead of their purpose. Most of your CSS is contained in really big and difficult-to-maintain chunks.

It’s a trap door - if you fall through, you’ll have a tough time getting back out. When you try to move things out of the page id scope, your carefully constructed mess of overrides falls apart. You’ll have to verify every page and state on the site.

Conclusion

Adding the controller and action names to the layout template was a neat trick, but led us to a bad place. Putting those names in an HTML comment would have accomplished the same thing, without giving us a hook that was so easy to misuse.

CSS is cleanest when its selectors include the absolute minimum, i.e. when components of the page do not depend on what contains them. If a component should look different when it’s on a different page, it should probably have a different class or id. Component identifiers should be specific enough that they don’t need to be scoped to a particular part of the application.

SASS made it very easy to do the wrong thing. Nobody would ever write the generated CSS by hand, it’s obviously unmaintainable. This should have been a clue.

I’m sure there are times that attaching CSS to a page-specific id is a great idea, but it’s a technique that should be used judiciously.