CSS Module Selectors Pile Up with postcss-import


Use of postcss-import in your CSS module processing can lead to more selectors than you realized. Here are some ways to deal with it.

postcss-import is a useful postcss plugin. It allows loading CSS from npm modules by looking up module entry via package.json's "style" attribute. But if you don't realize that one of its main features is inlining what you @import, the consequences can be unexpected. So what can you do?

If You Don't Use postcss-import

It's one option to just not use it. But the plugin does seem to smooth out importing. You're able to import from node_modules, web_modules, and utilize that package.json "style" attribute. So, I'm keeping it. Ok, where from here?

Scenario

Let's say that I have a source CSS file that I'm building into a module with css-loader, called source.css:

.mySelector {
  color: red;
}

And something that wants to be imported, dependency.css:

.dependencySelector {
  outline: 1px solid blue;
}

No @imports Yet

If I don't import my dependency, then the export object for the source.css CSS module will look like:

{
  mySelector: "mySelector___oHNuy"
}

This is as expected. No surprises.

Top-level @import

If we make sure our postcss.config.js is running with postcss-import:

module.exports = {
  plugins: {
    'postcss-import': {}
  }
}

And then add an @import of the dependency to our source.css:

@import "./dependency.css";
.mySelector {
  color: red;
}

We've done nothing to use the dependency or compose with it, but our module export object still changes to:

{
  dependencySelector: "dependencySelector___2HjUz"
  mySelector: "mySelector___oHNuy"
}

This is because postcss-import is inlining everything we @import.

Composes with Top-level Import

Once we use the imported dependency in a composes attribute:

@import "./dependency.css";
.mySelector {
  composes: dependencySelector;
  color: red;
}

We change the output of the mySelector value, where both selector values now are composed:

{
  dependencySelector: "dependencySelector___2HjUz"
  mySelector: "mySelector___oHNuy dependencySelector___2HjUz"
}

Composes From

But let's say that we want to avoid having dependencySelector appear in our CSS modules export object. We can use the composes: from syntax:

.mySelector {
  composes: dependencySelector from "./dependency.css";
  color: red;
}

This will yield the following CSS module export object:

{
  mySelector: "mySelector___oHNuy dependencySelector___2BS8K"
}

This is because the @import of the dependency is done within the scope of the .mySelector selector, not the entire module.

Import Variables

There's another way I've gotten around this issue. That is to @import only variables. If it is only variable values that you need, you can separate them into another file and import them without worrying about cluttering your CSS module export object. Since this is one of the style elements that we share most often, it works pretty well.

Let's make a new file, dependency-vars.css:

:root {
  --aDependencyVar: orange;
}

Then if I use it, even if imported at the top-level:

@import "./dependency-vars.css";
.mySelector {
  color: red;
  border: 1px solid var(--aDependencyVar);
}

It still yields an uncluttered CSS module export object:

{
  mySelector: "mySelector___oHNuy"
}

Have you run into this issue? Or is it a non-issue for you? What are some other ways that you've addressed it and kept your CSS modules uncluttered?