When we started working with React JS here at Octopus Energy, I thought I’d try implementing CSS Modules to achieve what they call ‘interoperable CSS’.
I used CSS modules as a PostCSS plugin and, with a few more plugins to handle things like mixins, nesting, and variables, I was ready to go.
Briefly, CSS Modules can be used with a React component as follows. Given a stylesheet for a component:
you can reference it in your component JSX file:
and reference the styles in the JSX:
Finally, during compilation, the CSS modules will ensure each reference is unique:
For a more detailed introduction to CSS Modules: Welcome to the Future by Glen Maddern
The benefits were pretty immediate. I had a
.css file to accompany every
.jsx file and I could use the sort of vague classnames you would
never dream of using in a regular CSS file. Suddenly
.Image was an
acceptable selector in the context, and one that wouldn’t result in me being
killed by another front end developer. The use of
@extend prevented code
duplication inside the component file, and allowed me to use styles from another
file if I was happy with reducing the level of encapsulation a little.
However, encapsulation means just that; totally encapsulated.
It’s important to note that the downside to CSS modules in this context is entirely our own doing. There were instances where React either wasn’t the best approach or wasn’t necessary for a particular section of the site. We have a lot of skilled Python developers at Octopus Energy and so it’s always smart to utilise that. Regardless of what is going on behind the scenes though, the front end is always expected to be consistent. However, I was now in the position where I had no way to access the randomly generated hash in the CSS selector that CSS modules creates and use it in a Django template.
Specifically, this part:
of this class name:
is dynamically injected before the page is rendered. I could try and guess the hash but I may as well have bought a lottery ticket and expected the same outcome - an unstyled component and no extra money.
The attempted workaround
So I had CSS siloed in modular component files and areas of the site that now
wanted to use those styles that weren’t React-based. In an attempt to prevent
excessive duplication between the two, I created a
Sitewide.css file that both
the CSS modules and the SASS (used for the rest of the site/global styles) could
extend from. The downside to this approach is two-fold:
1) The styles in this file had to be written in pure CSS as SASS and PostCSS have different syntax for mixins and variables.
2) As pure CSS, the bigger the file becomes the less maintainable it is without the use of pre-processor features such as variables, nesting, and mixins.
Therefore, for future code quality, I had to remove CSS modules from the setup and replace it with SASS globally. However its approach did teach me some good techniques that I brought over to the custom approach we use now.
The new approach
We use a version of the 7-1 pattern to lay out our styles and directories (although ours is only 5-1). It looks like this:
Although all styles are technically ‘global’ now, we try and make each component as encapsulated as possible, enabling it to be used throughout the application with no visible changes in appearance.
To achieve this, we have a set of rules when styling components new or existing. The rules are as follows:
1. Mimic the React component layout
As you can see from the structure above, within
sass/, we have a
components/ directory that mimics the layout of the React components folder in
app/components. Although this isn’t in the same directory as the JS, it still maintains CSS modules’ idea of style separation. The effect is you still always know where to find the styles specific to a React component; it has the same name!
2. Never use global classes
Each selector in a component
.scss file will start with the name of the component followed by the class name. i.e. if we were creating a
button class in a container called
JoinComponent, the class selector in
_JoinComponent.scss would be:
This way, the button styles are exclusive to the
JoinComponent component, and would not be caught up in specificity issues or accidentally overridden in another file.
@extend global classes for a reason
If your component uses a common piece of styling, such as a button or link, you should use the original selector rather than extending it just to give it a new name that starts with the name of the component. So this is no good:
The only reasons to
@extend a selector in this way are:
- If you wish to use the core styles but want to add some more on top. In this case extending is fine as it prevents code duplication.
- If the name of the core selector would make no sense in the context of the component. The markup of a component should be easy to read and therefore the selectors should make sense alongside the markup. An example of this would be using the exact same styles as a
.buttonbut it actually being an alert. In which case the following would be fine:
4. Do not nest classes
This rule only applies to component-specific
.scss files. The reasoning behind this is that your layers of specificity remain low, as you avoid cases where classes only get certain styling when they are inside other classes etc. Therefore if you ever changed the hierarchy of the component markup, it would break the styling.
You are allowed to style anything inside a class that is a regular HTML component (paragraph or anchor tags for example), but instead of nesting classes, simply create them as two separate selectors. The fact that each component selector starts with the component’s name also means that you can be vague in your selector names and not worry that the style will affect other areas of the app:
- Patterns of flakey Python tests
- Integrating Asana and GitHub
- Durable database transactions in Django
- Python interfaces a la Golang
- Beware changing the "related name" of a Django model field
- Our in-house coding conventions
- Recommended Django project structure
- Using a custom Sentry client
- Improving accessibility at Octopus Energy
- Django, ELB health checks and continuous delivery
- Testing for missing migrations in Django
- Hello world, would you like to join us?