CSS architecture is a complex subject that is often overlooked by developers, as it's possible to encapsulate CSS per component and avoid many of the common pitfalls that relate to CSS. While this 'workaround' can make the lives of developers simpler, it does so at the cost of reusability and extendibility.
When a developer defines a CSS class, it automatically affects the global scope modifying all related elements (and their children). This works great for simple applications where developers can predict the results, but can quickly become a problem when the size of the application and the team grows, and unintended results start to happen.
Initially, this problem was solved by Block Element Modifier (BEM), which is a methodology and set of naming conventions that helped avoid clashes and gave developers strong indications as to what each class did e.g. form__submit--disabled
tells us we are within a form, handling a submit button, and applying the disabled state.
However, following a naming convention is often hard to enforce, and when JavaScript offered solutions that were simpler to implement, developers accepted them with open arms. Solutions like CSS Modules or Styled Components take different approaches but solve many of the same problems BEM tackled by containing the CSS within a single component.
To address the lack of cross-application architecture in componentized design, we need to address three separate concerns:
- UI - which includes themes and general application behavior
- Layout Components - Often referred to as container or smart components, which are generally not reusable but determine how our components behaved in a specific scenario
- Presentational Components - these are the reusable pieces of code that power our applications. To increase their versatility, they include as little logic as possible
UI
The UI is defined in global CSS files that affect the entire application. It includes two main concerns:
1. Constants - Until recently, developers used SCSS or LESS variables, but these days we can use custom CSS properties that are supported by all major browsers.
CSS custom properties provide two important benefits. They can be modified at runtime, which is a perfect solution for switching themes or enabling dark mode, and they can be modified within our layout components, allowing developers to adjust the design on a smaller scale more easily.
2. Definition of UI state that can generally be broken down into three aspects:
- Modifier states - includes decisions such as size (large/small) or design (primary/secondary) variations of different elements.
- Behavioral states - includes app-wide states like online/offline, loading, etc.
- Pseudo states - temporary states like enabled/disabled that also include CSS states like
:hover
, or:focus
.
Layout Components
Layout components help put our reusable components to work by organizing them in a specific way on the page. As such, their responsibility is divided between initializing the reusable components with the particular properties and design, as well as setting the particular layout of the area they control using capabilities such as CSS Grid, Flexbox, etc.
Reusable Components
Reusable components have very little logic; they accept data from the layout components and trigger events (or callbacks) in the layout component when an action is taken.
To ensure these components are reusable, we try to include only the bare minimum of logic and design - which means that we generally avoid properties such as display, width, or margin.
This can be a difficult task, as it often requires handling more scenarios that the component was initially built for (e.g. a button that supports multiple lines, or a title with too much text), but ensures that the component is truly reusable and won't require re-writing the next time we use it in our application.