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:
- Allow you full modular control over all your CSS in an intuitive way
- 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:
- Every HTML page has a
<body>
element - The
<body>
element has both aclass
and anid
attribute (and we can modify those) - The Rails app uses the vanilla resource-oriented approach (see below for clarification)
- 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!