How to Build a Scalable CSS Architecture in React

November 01, 2020

illustration-of-blueprints

There is nothing more maddening than a disorganized CSS architecture. If you’ve worked on a large project with a significant amount of styling and components then you know full well the horror. You’ve got CSS styles bleeding all over the place, cruft from unused stylesheets of long-deleted components and “!important” everywhere. The whole thing is Kafkaesque – nothing makes sense and all logic seems to fly out of the window.

While there are many popular books on clean architecture for server side code, CSS is an often underlooked part of building an application. You can’t just plop everything in a CSS file and call it a day. The larger your program gets, the more unwieldy your styling becomes. A little bit of planning at the start can reap long-term benefits. But before we get into the solutions it is worth discussing the alternatives.

Something to note - this article discusses CSS architecture from a React perspective but the same principles can be used for other frontend frameworks.

Alternatives:

1. The basics - inline styles or CSS stylesheets

The simplest way to do styling in React is inline styles. It’s certainly not ideal for reasons that are apparent. Even the official React documentation doesn’t recommend it. I use this from time to time for prototyping when I can’t be bothered to create a separate CSS file or when I’m just adding one-off styles such as padding or margins.

import React from "react"

const MyComponent = () => {
  return (
    <div>
      <button
        style={{
          background: "transparent",
          borderRadius: "3px",
          border: "2px solid palevioletred",
          color: "palevioletred",
          margin: "5px",
          padding: "5px 10px",
        }}
      >
        Click me!
      </button>
    </div>
  )
}

export default MyComponent

Another basic approach is a vanilla CSS file. You could simply create a normal CSS file alongside your components and import it like so.

import React from "react"
import "./styles.css"

const MyComponent = () => {
  return (
    <div>
      <button className="primary-button">Click me!</button>
    </div>
  )
}

export default MyComponent
/* styles.css */
.primary-button {
  background: transparent;
  border-radius: 3px;
  border: 2px solid palevioletred;
  color: palevioletred;
  margin: 5px;
  padding: 5px 10px;
}

There are several issues with this approach. For one, the styles are globally accessible which means every component has access to the styles you create. In the above example, any other component that uses the class “primary-button” will inherit the styling from the styles.css file. Naming becomes a nightmare at a certain point and you’ll end up having to resort to using something like BEM.

2. Tailwind CSS

Tailwind is an interesting option. It’s a utility-first CSS framework which means that you get access to low-level utility classes instead of predesigned components like you would with Bootstrap or Bulma. You basically style components by applying classes.

I like Tailwind. I use it at work and on projects. Once you’ve gotten used to it, it is quick and easy to apply styles and prototyping especially becomes a breeze. You avoid context-switching by not having to change tabs to a separate CSS file whenever you need to apply styling. Plus, you don’t have to worry about common CSS issues like naming classes or maintaining CSS files. Here’s what a typical React component using Tailwind looks like:

import React from "react"

const MyComponent = () => {
  return (
    <div>
      <button className="bg-transparent rounded border-solid border-2 border-red-600 text-red-600 m-5 px-10 py-5">
        Click me!
      </button>
    </div>
  )
}

export default MyComponent

Unfortunately, it can also add a lot of clutter to your JSX. One of my priorities when creating React applications is to build small components that are easy to read and maintain. Having a bunch of Tailwind classes mixed in with the view and logic in the JSX can be a mess.

Of course, there are ways to keep the bloat down. Tailwind recommends that you extract commonly used styles into components. There is also the option to use @apply for commonly used styles like so.

<button className="btn-primary">Click me!</button>
.btn-primary {
  @apply bg-transparent rounded border-solid border-2 border-red-600 text-red-600 m-5 px-10 py-5;
}

However, not everything can be turned into a component and using something like @apply to encapsulate commonly used styles seems pretty close to doing things the old-fashioned way. If you can deal with seeing all those classes in your template then Tailwind is a good option, but to me it seems like it solved certain issues at the expense of creating new ones.

3. CSS-in-JS

CSS-in-JS is another option. There are several popular libraries including styled-components and Emotion. Basically you use JavaScript to style your components. Here’s an example of a React component using styled-components.

import React from "react"
import styled from "styled-components"

const PrimaryButton = styled.button`
  background: transparent;
  border-radius: 3px;
  border: 2px solid palevioletred;
  color: palevioletred;
  margin: 5px;
  padding: 5px 10px;
`

const MyComponent = () => {
  return (
    <div>
      <PrimaryButton>Click me!</PrimaryButton>
    </div>
  )
}

export default MyComponent

There are some advantages to this approach. Like Tailwind, you avoid the hassle of class name conflicts. Your styles are right there in the component and deleting the component also removes the styles making for an easy cleanup. You can also make use of props to dynamically change styles.

There is a bit of a learning curve though and personally I am not a fan of the syntax. But the biggest disadvantage in my opinion is that it clutters up your code. At a certain size you’re going to have a bunch of CSS-in-JS in your components which makes them difficult to read and manage.

My Solution: CSS modules with SASS and a global styles folder

CSS modules are basically CSS files where class names are locally scoped, meaning we can use whatever names we want in the file and not have it accessible to any other component. Naming has always been one of the pain points of CSS precipitating the need for solutions such as BEM. CSS modules takes that worry out of the equation. Now you can have 50 “wrappers” or “containers” scattered throughout your CSS and as long as you are using CSS modules the styles shouldn’t affect each other.

CSS modules are also more maintainable. The modules are usually housed in the same folder as the component making them easy to find. This makes them easy to remove in case a component is deleted, thereby preventing a bunch of unused stylesheets.

The winning factor for me is the clear separation of concerns between the styling and the view. Here’s an example of component using CSS modules.

import React from "react"
import styles from "./MyComponent.module.scss"

const MyComponent = () => {
  return (
    <div>
      <button className={styles.primaryButton}>Click me!</button>
    </div>
  )
}

export default MyComponent
/* MyComponent.module.scss */
.primaryButton {
  background: transparent;
  border-radius: 3px;
  border: 2px solid palevioletred;
  color: palevioletred;
  margin: 5px;
  padding: 5px 10px;
}

As you can see, the JSX becomes much more readable. The component is lean and concise. All the styling is confined to MyComponent.module.scss.

Last but not least, CSS modules are familiar. While Tailwind and CSS-in-JS libraries are certainly all the rage right now, they are also yet another thing you have to learn and get used to. If you’re working on a team that’s not already familiar with these tools, you have to be able to make a strong case as these are not insignificant shifts for someone who has been doing frontend styling the traditional way for years. CSS modules allows you to apply styling the way you are used to while still reaping the benefits of scalability and maintainability.

As you may have noticed, I used SASS for my CSS modules. While it’s certainly not required, SASS gives you a lot of benefits including variables, nesting and mixins. It’s also been around for a while and many front-end developers likely have experience in it.

With this approach you will also need a global styles folder. Global styles are still necessary in order to implement styles that are shared across many pages or components or even throughout your entire site such as layouts, colors and typography. This styles folder can be kept pretty lean as the styling for most of the components would live right next to the jsx file in the components folder. I recommend using the 7-1 pattern to organize your global styles folder.

Make an Informed Decision

If I haven’t been able to convince you of the merits of CSS modules, I recommend starting a small project and implementing styles in each of the different strategies above. This will give you a better idea of what you like or don’t like about each approach.

Organizing your styles shouldn’t be an afterthought. Thinking of clean CSS architecture early on is the key to happy developers, cleaner, more maintainable code and a better user experience.

Illustration by Thierry Fousse from Icons8