Photo

Hi, I'm Aaron.

Implementing Modular CSS in Ruby on Rails

The Problem

CSS cascades. This is a great feature of it! But it can be tricky when you have a single monolithic CSS file and want some styles to apply to certain pages but not to other pages.

For example, let’s say we had an article summary block on the homepage of the site:

1
2
3
4
5
6
7
<% @posts.each do |post| %>
  <article>
    <h2><%= post.title %></h2>
    <p><%= post.summary %></p>
    <%= link_to post, 'Read More' %>
  </article>
<% end %>

and wanted to apply these styles to the heading:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
article {
  h2 {
    background-color: $brand-primary;
    font-size: $large;
  }
  p {
    padding: 1rem;

    & + a {
      display: block;
      margin-top: 1em;
    }
  }
}

(I threw this up on a codepen)

Cool! But maybe on the :posts resource itself we want to display multiple paragraphs at a larger width, and also use a different background color:

1
2
3
4
5
6
7
8
9
10
article {
  width: 50%;
  
  h2 {
    background-color: #654321;
  }
  p {
    padding: 0;
  }
}

(codepen)

Looks great! But uh-oh….

With those changes, we now have the :posts resource styles applying to the homepage styling as well. Context collapse!

(codepen)

Of course, this specific situation can be resolved by adding an article class (perhaps <article class="summary"> for the homepage styles and <article class="full-text"> for the full view version). This situation is also oversimplified, to illustrate the context collapse. What happens when you start adding in !important styles, or want to do a style override that can’t be solved easily by changing load order or merely applying a class?

What follows is a solution I developed a few years back and it will:

  1. Allow you full modular control over all your CSS in an intuitive way
  2. Be more or less future-proof (when you add a new resource, the SCSS file created for that resource is what you’ll use)

Bonus: you can technically use this to address similar modularization of JS snippets for simple JS scripts (this is a bit harder to integrate with more complicated JS frameworks).

Modularity Hooks

To gain modularity in our CSS we need to give our application hooks that are contextual to the controller’s action that we’re viewing.

We begin with these assumptions:

  1. Every HTML page has a <body> element
  2. The <body> element has both a class and an id attribute (and we can modify those)
  3. The Rails app uses the vanilla resource-oriented approach (see below for clarification)
  4. The Rails stack will provide us with some params set at load-time, indicating which Controller and Action are being used for this request.

The reductive TL;DR is: We set the body#id to the Controller and the body.class to the Action.

I think using the body tag makes the most sense, semantically, because CSS (practically) only really applies to the content, and the body is the root of the content in the document. You could, in theory, use the <html> tag itself, though I personlly think that makes less semantic sense for this.

In the code, the trivial / naive approach, that we will harden up a little later, is like this:

1
2
3
<!-- app/views/layouts/application.html.erb -->
<body id="<%= params[:controller] %>" 
   class="<%= params[:action] %>">

And then in your SCSS, you can now create controller- and even action-specific styles:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/assets/stylesheets/users.scss -->
body#users {
  &.new {
    header#banner {
      background-image: url('new-user.jpg');
    }
  }
  &.edit {
    header#banner {
      background-image: url('edit-user.jpg');
    } 
  }
}

And those styles won’t conflict with other controller actions:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/assets/stylesheets/projects.scss -->
body#projects {
  &.new {
    header#banner {
      background-image: url('new-project.jpg');
    }
  }
  &.edit {
    header#banner {
      background-image: url('edit-project.jpg');
    } 
  }
}

Boom.

For very simple Rails applications that only have shallow top-level resources, this is it; you’re done.

Dealing with Namespaces

Sometimes you want to do namespaces, like an /admin/* namespace. Specifically, I’m talking about situations where you have something like app/controllers/admin/*.rb, the routes have namespace :admin do # ..., and the controllers within that each are like Admin::SomeController:

In these cases, the params[:controller] will give us: admin/users, which doesn’t evaluate so cleanly in SCSS. So let sanitize that using :gsub:

1
2
3
<!-- app/views/layouts/application.html.erb -->
<body id="<%= params[:controller].gsub(/[^\w\d]+/,'--') %>" 
   class="<%= params[:action] %>">

And now in the SCSS, we can do things like:

1
2
3
4
5
6
7
8
9
<!-- app/assets/stylesheets/admin/users.scss -->
body#admin--users {
  &.edit {
    header#banner {
      background-color: orange;
      background-image: url('new-user.jpg');
    }
  }
}

Security Considerations

Since we’re dumping the params[:action] directly into the source, we could pre-emptively apply a similar sanitization to it just to be safe:

1
2
3
<!-- app/views/layouts/application.html.erb -->
<body id="<%= params[:controller].gsub(/[^\w]+/,'--') %>" 
   class="<%= params[:action].gsub(/[^\w]+/,'') %>">

Generally, you don’t want to reveal more about your surface than you need to. Exposing the name of the controller resource and the name of the action might feel like a security risk. Anyone interested in attacking your rails application can probably ascertain your controller names by looking at other resources, so the marginal exposure from using these in the HTML source is very likely minimal.

That said, if your application’s routes do not directly mirror your controller names (eg. you have /players pointing to UsersController, rather than PlayersController), and if you are doing that intentionally because you think this obscurity gives you additional security (it very likely does not), then this approach for modular assets likely won’t fit your needs.

Attribute Collision

It’s possible you are already using body#id and body.class for other purposes – this is totally understandable but thankfully there is an easy work-around: Use data-attributes. You can even go this route anyways if you prefer to keep your data presentation more organized and are put off by using id and class attributes in this way.

It might look like this:

1
2
3
4
5
<!-- app/views/layouts/application.html.erb -->
<body         id="whatever_you_need_it_to_be" 
           class="this-too" 
   data-resource="<%= params[:controller].gsub(/[^\w]+/,'--') %>" 
     data-method="<%= params[:action].gsub(/[^\w]+/,'') %>">

and then referencing it in your CSS like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/assets/stylesheets/users.scss -->
body[data-resource="users"] {
  &[data-method="new"] {
    header#banner {
      background-image: url('new-user.jpg');
    }
  }
  &[data-method="edit"] {
    header#banner {
      background-image: url('edit-user.jpg');
    } 
  }
}

This accomplishes the same thing but with a little more specificity at the cost of a little more typing. I actually like this one a lot because the intention is a lot more semantically clear, though I also admit it’s not quite as elegant as the initial solution.

Non-Resourceful Routing

If your Rails app uses non-resourceful controllers, e.g. UserSignupsController, but those controllers are manipulating a different resource, e.g. User, you can still use this approach though you’ll need to do a bit more maintenance to keep it up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- app/assets/stylesheets/users.scss -->
body#users,
body#user-signups {
  &.new {
    header#banner {
      background-image: url('new-user.jpg');
    }
  }
  &.edit {
    header#banner {
      background-image: url('edit-user.jpg');
    } 
  }
}

Personally, I dislike non-resourceful controllers. I would rather have a :signup action on the UsersController than a :new action on a UserSignupsController, unless I’m creating a UserSignup model…but in that case, it’s now a resourceful route again.

This also assumes you’re wanting to present UserSignups and Users the same – but if UserSignups manipulates both User and also, say, Account models, you could be creating a limited context-collapse. This is part of the reason I dislike non-resourceful routes, though.

Implementation Summary

Step 1: Add this to your main layout

Whichever file begins with <html>, and has a big, expectant, <%= yield %> somewhere within the <body> tag, that’s the one you want to modify:

If you aren’t currently setting the id attribute of the Body tag:

1
2
3
<!-- eg. app/views/layouts/application.html.erb -->
<body id="<%= params[:controller].gsub(/[^\w]+/,'--') %>" 
   class="<%= params[:action].gsub(/[^\w]+/,'') %>">

If you have additional classes, append those after the ERB insert.

If you are currently setting the id attribute of the Body tag:

1
2
3
4
5
<!-- app/views/layouts/application.html.erb -->
<body         id="..." 
           class="..." 
   data-resource="<%= params[:controller].gsub(/[^\w]+/,'--') %>" 
     data-method="<%= params[:action].gsub(/[^\w]+/,'') %>">

Step 2: Wrap your resource SCSS files with the appropriate context

For “some_resource”, that has actions: “new”, “edit”, and “register”, you would use these wrappers:

If you aren’t currently setting the id attribute of the Body tag:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- app/assets/stylesheets/some_resource.scss -->
body#some_resources {
  &.new {
    // ...
  }
  &.edit {
    // ...
  }
  &.register {
    // ...
  }
}

If you are currently setting the id attribute of the Body tag:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- app/assets/stylesheets/some_resource.scss -->
body[data-resource="some_resources"] {
  &[data-method="new"] {
    // ...
  }
  &[data-method="edit"] {
    // ...
  }
  &[data-method="register"] {
    // ...
  }
}

That’s it!

If you already have a bunch of SCSS written and it’s scattered all over the place, this can be a non-trivial migration, but I promise it’s worth it for the organization alone.

Good luck!