How can I use CSS Modules with Rails 8 and PostCSS?

I would like to be able to link CSS Modules at the layout/partial level in order to build my own components and themes that I can use in different projects.

I still can’t wrap my head around Import Maps, CSSBundling Rails, Propshaft, and JSBundling Rails.

How does it all fit together?

It seems like the official documentation on these is pretty minimal. And all the guides online are all about Tailwind CSS, but I would rather write vanilla CSS and opt into a few features like CSS Modules.

I’m new here so please let me know if this is the wrong place to post this.

Any help would be much appreciated!

To link CSS Modules at the layout/partial level and better understand Rails’ modern asset handling tools, here’s a breakdown:

  1. CSS Modules: Use tools like CSSBundling-Rails to process your CSS (including CSS Modules). It lets you write vanilla CSS while enabling modular scoping.
  2. Import Maps: Handles JavaScript dependencies without a bundler. Import Maps is unrelated to CSS Modules but works well if you’re managing JS for interactive components.
  3. Propshaft: A modern replacement for Sprockets, Propshaft handles asset pipelines in Rails, focusing on simplicity (e.g., direct file serving). Use it for linking assets like CSS/JS.
1 Like

Thank you for your reply. I managed to install CSSBundling-Rails but I don’t know how to import my classes into my layout or partial. And I can’t seem to find any documentation about it.

I like Importmaps. Can I make PostCSS work with that?

I’m unfamiliar with CSS Modules but from what I can tell they allow you to generate unique CSS classes in JavaScript. Partials/Layouts are not generated by JavaScript, they are just html that gets generated via a Ruby template engine (most often ERB).

So to import css modules into a partial or layout I would use a script tag (or stimulus controller if using stimulus.js) that imported the css module and then applied the module class names to the DOM directly. You’d need to figure out how to map those on your own, such as with a custom data attribute in your html. It looks like postcss-modules generated a JSON map that can help in that mapping process , but you still have to manually apply classes unless you’re using a FE framework such as react or vue that compiles your html from JSX or an equivalent.

1 Like

I think I figured it out with the help of Joel Draper and Joel Moss over at Phlex’s github! [Link]

I’ll paste my solution here for anyone who’s interested.

  1. Co-locate JS, CSS, and component files for each component. For example:
├── app
│   ├── views
│   │   ├── components
│   │   │   ├── navbar
│   │   │   │   ├── navbar.css
│   │   │   │   ├── navbar.rb
│   │   │   │   ├── navbar.js
  1. Use PostCSS Modules to transform any classes found under views before bundling. For example class="container" becomes class="Components--Navbar--container"
// /postcss.config.js
const path = require('path');
module.exports = {
  plugins: [
    require('postcss-import-ext-glob'),
    require('postcss-import')({
      plugins: [
        require('postcss-modules')({
          generateScopedName: (name, filename, _css) => {
            const path = require('path');
            return path.relative('./app/views/', filename).
                split('/').
                toSpliced(-1).
                map((f) => String(f).charAt(0).toUpperCase() + String(f).slice(1)).
                join('--') + "--" + name;
          },
          // Don't generate *.css.json files (we don't need them)
          getJSON: () => {},
        })],
    }),
    require('postcss-nesting'),
    require('autoprefixer')
  ],
};
  1. Use a helper to transform CSS classes in the component the same way as above. Export the module function so it can become an instance method in step 4.
# /app/helpers/css_helper.rb
module CssHelper
  def modularize(class_name)
    component = self.class.name.gsub("::", "--")
    component + "--" + class_name
  end

  module_function :modularize
end
  1. Include the helper module in the Components::Base
# /app/views/components/base.rb
class Components::Base < Phlex::HTML
  include Components
  include Phlex::Rails::Helpers::Routes
  include Phlex::Rails::Helpers::ImageTag
  include Phlex::Translation
  include CssHelper # <==
end
  1. Call the helper from the component when defining HTML element classes.
# /app/views/components/navbar/navbar.rb
class Components::Navbar < Components::Base

  def view_template(&)
    div(class: modularize("container"), &)
  end
end

This is what the CSS file would look like pre-processing:

/* /app/views/components/navbar/navbar.css */
.container {
  background-color: blue;
}

And post:

/* /app/assets/builds/application.css */
.Components--Navbar--container {
  background-color: blue;
}

The helper we defined above will transform our HTML to match! The benefit of this solution is that we are shipping plain, functional CSS and HTML with scoped classes. This is better than trying to update our HTML in the client via JS. With that said, it’s a very complex solution. I’m curious about the Web Components approach described in the ViewComponent docs