Scalable CSS architecture

For years I have used ITCSS as my goto CSS architecture for large projects. It helped me to keep my CSS maintainable with a small team. But in the last two years, I moved to a utility-first approach. More and more parts of ITCSS were left untouched and unused. At this point, I came across the CUBE CSS of Andy Bell. It is the methodology describing how I was, and still am, implementing CSS. So as every self-respecting front-end developer with an online presence, I took it, changed it, created a framework, and wrote about it!

Core principles

The framework tries to achieve simplicity for developers. To achieve this, everything is designed around three core principles.

Architecture

A simple three-layered architecture that can be used as only your CSS architecture, but can be extended towards a design system (by combining it with front-end frameworks like React in the ‘components’ layer). It heavily focuses on layout patterns above anything.

The modern trend of utility classes is heavily supported in this architecture. Even the layout patterns can be implemented as utilities. They can be accompanied by class utilities, dedicated to changing one small property of the layout pattern (e.g. .switcher-w-0 sets the width of the switcher pattern). These class utilities impact internal CSS custom properties, to avoid collision with other classes as much as possible.

styles/
├── components/     // all components
├── layout/         // classes for layout patterns
├── utilities/      // utility classes
├── _global.scss    // global styles targeting HTML tags
├── _reset.scss     // CSS reset
├── _tokens.scss    // design tokens
└── index.scss

Design tokens

A big part of the framework is the correct usage of design tokens. These tokens are used to create a consistent result across the implementation. Design tokens can be ‘literal’ (exact values) or ‘derived’ from literal tokens. All tokens follow the same naming convention --<type>-<category>-<number>. The type indicates what the token impacts (e.g. color). The category is an optional level when the type does not suffice or can collide with properties. The number is used to show that we are increasing something of the type. This makes implementation easy for developers, as you do not have to know exactly what value corresponds to the number. The lowest available number is 0.

There are a few different types of design tokens existing within the framework.

As the framework is extensible, other tokens can be defined as well, such as font-families. To ensure scalability, CSS custom properties are used as the baseline, to allow the tokens to be used everywhere consistently. SCSS can be used to define the custom properties more easily, but it is mainly used to generate utility classes.

$colors: (
  "black": #000,
  "white": #fff,
);

:root {
  @each $name, $color in $colors {
    --#{$name}: #{$color};
  }
}

@each $name, $color in $colors {
  .bg-#{$name} {
    background-color: var(--#{$name});
  }
}

Components

Components are CSS classes created to fill the gaps utility classes cannot fill. They group several CSS properties. Where possible the defined CSS custom properties based on the design tokens are used. Components can be more than CSS only, though. It can be a combination with actual UI components through a JavaScript framework (e.g. React). All (CSS) components follow a simple functional pattern.

Every system has these generic components that you see coming back. Buttons, input fields, tables, you name it. These components are called foundational components. Foundational components exist in four different categories.

Next to foundational components you have application components. These are non-generic components. They cannot be shared between applications. They are often a combination of foundational components, or deviate from the foundational rules. No pre-defined categories exist for these components, but you can make them based on common sense.

Co-location and data-* attributes

Where possible, components should be co-located with the actual UI components. Several frameworks support this directly (e.g. Svelte), or CSS Modules can be used to achieve a similar effect as well.

components/
├── button/
    ├── Button.js
    └── button.module.scss
// Button.js
import styles from "./button.module.scss";

export default function Button() {
  return <button className={styles.btn}>...</button>;
}

For both the type and state of components, I advise using data-* attributes, as mentioned. These allow a flexible and maintainable way to build your components. The ~= used in the snippet below allows CSS to check if the value (e.g. touched) exists in a space-separated list of strings when used. With the snippet below, it possible to have data-state="touched error" on an input field, and have both style definitions applied. The i at the end ensures everything is evaluated without case-sensitivity. These attributes can also be combined with CSS Modules.

.input { ... }
/* case-insensitive, with value check in list of strings */
.input[data-state~="touched" i] { ... }
.input[data-state~="error" i] { ... }
.input[readonly] { ... }
.input:hover { ... }

Wrapping up

The moment I read about CUBE CSS, I was a fan of the methodology. How could I not? It was describing how I felt about CSS and how I was using it. At the same time, I became a big fan of customer properties. So why not combine the two into a framework? Which is what I did. The current version of the framework is open on GitHub. It is small but used in several projects, including this website. It has several layouts and utility classes built in. For now, I intend to continue to improve and enrich the framework when I can. Let me know in the GitHub issues what you think should be added!