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:

  1. Establish a reset
  2. Create a base set of styles
  3. Start building in application.css
  4. Refactor and extract as needed
  5. 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";
app/frontend/entrypoints/application.css
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%;
}
app/frontend/stylesheets/reset.css
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);
}
app/frontend/stylesheets/base.css
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";
app/frontend/entrypoints/application.css
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, like stylesheets/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:

  1. 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 in home.css.
  2. __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.
  3. --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
  }
}
.stylelintrc.js
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'
Assuming you have a recent version of Node installed, 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.

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/rails-template

App template for Rails 7 projects; best practices for TDD, security, deployment, and developer productivity. Now with optional Vite integration! ⚡️

1,055
Updated 1 month ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 20 days ago

More on GitHub →