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’.
It works especially well with React components, which are re-usable units of mark-up and Javascript functionality. Using CSS modules allows a component’s styles to be bundled with the component so they can be re-used through your site.
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
component .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.
The issue
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:
The rules
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.
3. Only @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
.button
but 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:
- Estimating cost per dbt model in Databricks
- Automating secrets management with 1Password Connect
- Understanding how mypy follows imports
- Optimizing AWS Stream Consumer Performance
- Sharing Power Updates using Amazon EventBridge Pipes
- Using formatters and linters to manage a large codebase
- Our pull request conventions
- 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?