css
How to organize CSS in a Rails project
Don’t overthink it! Start with a simple base set of styles and refactor by extracting files and shared components as you go.
New Rails projects start with a single CSS file: application.css
. Inevitably every non-trivial app is going to have more CSS than will fit comfortably in that one file. While you may be tempted to declare a comprehensive file and folder structure up front, I suggest starting small and taking the following incremental approach:
- Establish a reset
- Create a base set of styles
- Start building in
application.css
- Refactor and extract as needed
- Follow class naming conventions
1. Establish a reset
As I explained in an earlier post, every app should start with a normalize stylesheet and a lightweight CSS reset. This can provide the basis of your initial file structure.
application.css
is the main entrypoint for all CSS. From here you can import other stylesheets.stylesheets/reset.css
can contain your CSS reset. This is “set it and forget it” boilerplate, so it makes sense to stash it away in its own file.
Here’s a good starting point:
@import "modern-normalize";
@import "~/stylesheets/reset";
The main entrypoint can import modern-normalize (an npm package) and your reset stylesheet. (Note: I’m using Vite import semantics for the examples in this article.)
Your reset stylesheet might look like this:
:root {
line-height: 1.5;
}
h1, h2, h3, h4, h5, figure, p, ol, ul {
margin: 0;
}
ol, ul {
list-style: none;
padding-inline: 0;
}
h1, h2, h3, h4, h5 {
font-size: inherit;
font-weight: inherit;
}
img {
display: block;
max-inline-size: 100%;
}
This clears out some of the default browser styles to establish more of a “blank canvas” for the rest of your CSS. See my CSS reset article for more details.
2. Create a base set of styles
With the normalize and reset boilerplate in place, you can now start laying down the styles that are more unique to your project.
I recommend creating a special base.css
file for specifying global styles. Here are things that make sense to put in the base.css
stylesheet:
- The
body { font-family: "..." }
to set your default typeface. - Any
@font-face
declarations, if you are loading custom web fonts. - Your default foreground, background, and link colors.
- Global CSS variables, such as your color palette.
That’s pretty much it! If you are building and styling your app in a class-oriented fashion (as explained in the next section), then your base stylesheet will be fairly small. Here’s an example:
:root {
--blue-500: #0679cc;
}
body {
font-family: "Inter var", sans-serif;
}
a:any-link {
color: var(--blue-500);
}
The
base
stylesheet is the place to set global variables, fonts, and colors.
Don’t forget to import base
from the entrypoint file:
@import "modern-normalize";
@import "~/stylesheets/reset";
@import "~/stylesheets/base";
The
base
stylesheet should be imported after reset
.
3. Start building in application.css
In my experience, trying to define abstractions or identify reusable bits of CSS early in a project can lead to dead-ends. It’s best to prioritize speed of delivery at first, even if the code can feel a bit messy.
Patterns and abstractions in a UI design take time to emerge. It’s natural for things to be a little inconsistent and uncertain at the beginning of a project. It follows that your CSS should be loose and not overly-structured at this point.
I suggest dumping all of your CSS classes in the application.css
file to start. Follow these guidelines so that refactoring will be easier later:
- Use classes exclusively. Don’t apply styles to bare elements like
p
; assign a class like<p class="intro">
and style the class instead. - Avoid descendant (nested), child (
>
), or sibling (+
) selectors. Create a new class for each element you need to style. That way when you later change the markup or extract partials, it will be easier for the CSS to move along with it. - If you are styling elements that are closely related, give them a common class name prefix. This will make it easier to group and extract CSS down the road. For example, a navigation menu item only makes sense within the context of the menu that contains it. You can make that relationship clear by giving them both a
menu
prefix:.menu
,.menu__item
, etc. - Don’t use
#id
selectors or!important
.
Once application.css
starts getting too big, perhaps 200 lines or so, then it’s a good time to take a step back and consider refactoring.
4. Refactor and extract as needed
In Ruby or JavaScript, when technical debt starts to accumulate, refactoring is often used to address the underlying problems. Keep in mind that CSS is also a programming language (albeit a pretty unique and esoteric one), and refactoring can be used in CSS, too.
Let’s say your application.css
has grown to 200 lines and is starting to feel a bit unruly. Here are some refactoring approaches to consider:
- Does one page of your app feel different from the others? Maybe the home page is the odd one out. Move page-specific CSS to a separate file, like
stylesheets/home.css
. - Do you have HTML partials? For example, maybe you’ve created a
app/views/shared/_footer.html.erb
partial already. Find the CSS that affects that partial and extract it into a CSS component of the same name, likestylesheets/components/footer.css
. - Is there a common visual element that is repeated over and over? Many apps have a grid of “cards”, for example. Those styles could be extracted to
stylesheets/components/card.css
.
After a round of refactoring, your entrypoint might look like this:
@import "modern-normalize";
@import "~/stylesheets/reset";
@import "~/stylesheets/base";
@import "~/stylesheets/home";
@import "~/stylesheets/login";
@import "~/stylesheets/components/alerts";
@import "~/stylesheets/components/card";
@import "~/stylesheets/components/footer";
With this file structure:
app/frontend
├── entrypoints
│ └── application.css
└── stylesheets
├── components
│ ├── alerts.css
│ ├── card.css
│ └── footer.css
├── base.css
├── home.css
├── login.css
└── reset.css
Before getting too deep into refactoring, be sure to consult with the UI designer or product designer on your team. When elements of a page seem to be visually similar, that might be the designer’s intention, or it might be coincidental. If you refactor CSS to DRY up elements that are similar only by coincidence, that abstraction will often come back to bite you.
5. Follow class naming conventions
CSS is known for becoming unmaintainable spaghetti code for a few reasons:
- Overly-broad selectors (
p
,table
, etc.) and complex selectors (descendants, siblings, etc.) can have wide-reaching consequences. That makes refactoring or deleting CSS code risky, because it is not immediately clear if/how the styles affect every part of the app. - Styling is dependent on declaration order and specificity rules, which can be frustrating to manage when multiple style declarations are competing to style an element.
- CSS classes belong to a global namespace, making them prone to collisions. If two developers both write
.intro
styles in their own separate CSS files, for example, they can clobber each other’s work without realizing it.
Writing styles exclusively using class names, as explained in the previous section, goes a long way to address these CSS shortcomings. Using class names means that all rules have the same specificity, removing an entire category of problems. Avoiding complex selectors means there is a clear 1:1 mapping of CSS rule to the HTML elements that it affects.
However, class name collisions can still be a problem. This is where naming conventions help.
Match class names with file names
Ruby constants are also prone to naming collisions. That’s one reason why Rails has a strong file naming convention. The auto-loading mechanism enforces, for example, that app/models/user.rb
must define a class named User
.
Your CSS classes should follow a similar convention. Make sure that all classes in home.css
follow the pattern of home__*
, footer.css
contains only footer__*
classes, and so on. This will make your project easier to navigate and eliminate any confusion about what goes where.
Consider using the BEM convention
I recommend following BEM, which is a popular naming convention for CSS classes. It dictates that class names use the following pattern:
.block__element--modifier {
/* styles */
}
It can be understood as three simple rules:
block
is the name of a component or grouping of related classes. It should match the CSS file name. For example,.home
,.home__avatar
,.home__intro
, and.home__links
belong together inhome.css
.__element
is the name of element that is a related descendant of the block. Continuing the previous example, if.home
is the container, then.home__avatar
,.home__intro
, and.home__links
are three distinct elements within that container.--modifier
is attached to a class name to represent visual variations of the same element. For example,.flash__message--notice
and.flash__message--alert
would be two slightly different styles of a.flash__message
element.
The BEM quick start has a more in-depth explanation of these concepts with good examples.
Run Stylelint to flag duplicate classes
Stylelint is a linting tool with dozens of rules targeting CSS, similar to what Rubocop does for Ruby. A particularly useful Stylelint rule for detecting naming collisions is no-duplicate-selectors, which detects if an identical class name expression is declared twice within the same file.
Setting up Stylelint is outside the scope of this post, but if you want to quickly experiment you can create a basic .stylelintrc.js
file, like this:
module.exports = {
rules: {
"no-duplicate-selectors": true
}
}
This example enables a single rule. In practice, I recommend using the stylelint-config-standard package, which enables
no-duplicate-selectors
among several others.
Then run:
$ npx stylelint 'app/**/*.css'
npx
will download all necessary packages and then run the stylelint
executable.
Check out my rails-template project for an example of a more full-featured Stylelint config.
Parting thoughts
There are many techniques for styling Rails apps with CSS, including component-oriented sidecar files in ViewComponent, or utility-class systems like Tailwind. However, even with these solutions, you will still probably need a traditional CSS entrypoint and perhaps a handful a base styles.
My hope is that the structure and approach described in this post can scale with you from those lightweight use-cases all the way up to a full-blown custom design system.