039: CSS Architecture and Tooling — BEM, Sass, PostCSS, CSS Modules, and CSS-in-JS
Learning outcomes
By the end, you can organize growing CSS; explain BEM; understand what Sass and PostCSS add; understand CSS Modules and CSS-in-JS trade-offs; choose tools based on project constraints; and avoid using tooling to hide weak CSS fundamentals.
Prerequisites and retrieval
Retrieve selector specificity from 019–020, custom properties from 034, and component ownership from the capstone in 032.
Architecture first: define ownership
A maintainable stylesheet answers:
- Which selector owns this visual rule?
- Is this a global default, layout relationship, component, state, or utility?
- How can a component be changed without knowing five ancestors?
- Where should tokens live?
- How are third-party styles controlled?
One simple source order:
/* 1. reset */
/* 2. base */
/* 3. layout */
/* 4. components */
/* 5. utilities */
/* 6. overrides, if truly necessary */
Modern cascade layers can make that order explicit later.
BEM
BEM stands for Block, Element, Modifier.
<article class="card card--featured">
<h2 class="card__title">Pro plan</h2>
<p class="card__price">$29</p>
</article>
.card {}
.card__title {}
.card__price {}
.card--featured {}
Strengths:
- class ownership is explicit;
- selectors stay low-specificity;
- styles are relatively independent of DOM depth.
Costs:
- class names can be verbose;
- teams can over-formalize tiny components;
- BEM naming does not solve every cascade or design-system issue.
Use BEM as a naming convention, not a religion.
Worked example: fragile selector to component API
Fragile:
.pricing section article > div h3 {
color: purple;
}
Stable:
.price-card__title {
color: purple;
}
Markup can change from <h3> to another appropriate heading level without coupling presentation to remote structure.
Sass / SCSS
Sass is a preprocessor that compiles into CSS.
SCSS example:
$brand: #2563eb;
.card {
border: 1px solid #cbd5e1;
&__title {
color: $brand;
}
&--featured {
border-color: $brand;
}
}
Compiled output is ordinary CSS.
Historically Sass provided variables, nesting, mixins, functions, modules, and more before native CSS had alternatives. Modern CSS now has custom properties, nesting, math functions, cascade layers, and other capabilities, so use Sass features when they still add clear build-time value.
Sass variable versus CSS custom property
Sass:
$brand: #2563eb;
resolved at build time.
CSS:
:root {
--brand: #2563eb;
}
exists at runtime and participates in cascade/inheritance.
Choose based on whether the value must change dynamically by selector/theme/runtime.
PostCSS
PostCSS is a tool ecosystem that parses/transforms CSS through plugins.
A build may use PostCSS for:
- vendor prefix insertion;
- future-syntax transformations depending on policy;
- linting/optimization pipelines;
- framework processing.
Do not think of PostCSS as one CSS language. Its behavior depends on configured plugins.
CSS Modules
A component stylesheet:
/* Card.module.css */
.card {
padding: 1rem;
}
.title {
font-weight: 700;
}
In an application build, imported class names can be locally scoped/hashed.
Conceptual React-style usage:
import styles from "./Card.module.css";
export function Card() {
return <article className={styles.card}>...</article>;
}
Benefits:
- local class-name scope;
- fewer global naming collisions;
- ordinary CSS syntax.
Costs:
- build-tool dependency;
- global/shared styling needs deliberate handling;
- generated class names can complicate debugging until the team understands the mapping.
CSS-in-JS
Conceptually:
const Button = styled.button`
background: ${props => props.primary ? "blue" : "gray"};
`;
Different libraries implement CSS-in-JS differently: runtime injection, build-time extraction, object syntax, tagged templates, atomic classes, etc.
Potential benefits:
- co-location with components;
- dynamic values from application state;
- scoped/generated styles.
Potential costs:
- runtime overhead in some approaches;
- framework/library lock-in;
- server rendering/hydration complexity;
- harder browser-native debugging;
- duplicated abstractions when CSS already handles the state.
Do not choose CSS-in-JS simply because a project uses React.
Native CSS can now handle many old tooling use cases
Custom properties:
:root {
--brand: #2563eb;
}
Native nesting:
.card {
padding: 1rem;
& .card__title {
font-weight: 700;
}
}
Cascade layers:
@layer reset, base, components, utilities;
Container queries:
@container (width >= 30rem) { ... }
Tooling should solve project problems that remain after using platform features appropriately.
Choosing an approach
Plain CSS
Strong choice when:
- project size is manageable;
- platform CSS is sufficient;
- no scoping/build transform is needed.
BEM
Useful when:
- global CSS files need naming discipline;
- classes are hand-authored across templates/components.
Sass
Useful when:
- build-time functions/mixins/modules provide real value;
- existing codebase already depends on it.
PostCSS
Useful when:
- transformation/linting/prefixing pipeline is required.
CSS Modules
Useful when:
- component-local class scoping fits the framework/build.
CSS-in-JS
Useful when:
- a chosen library solves dynamic/co-location/design-system needs better than simpler alternatives.
Worked example: architecture with layers plus BEM
@layer reset, base, components, utilities;
@layer base {
body {
font-family: system-ui, sans-serif;
}
}
@layer components {
.alert {
padding: 1rem;
border-inline-start: 4px solid var(--alert-accent);
}
.alert--danger {
--alert-accent: #dc2626;
}
}
@layer utilities {
.sr-only {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
Naming convention and cascade architecture solve different problems and can coexist.
Deep dive: architecture is a dependency graph
CSS architecture is less about folder names and more about controlling dependency direction.
A useful order is:
- reset/normalization;
- design tokens;
- element defaults;
- layout primitives;
- components;
- utilities;
- narrowly scoped overrides.
Cascade layers can make that order explicit:
@layer reset, tokens, base, layout, components, utilities;
Selectors inside a component should depend primarily on the component's own API, not on distant page structure.
Fragile:
.dashboard .right-column .orders .card h3 {
...
}
Stronger:
.order-card__title {
...
}
Sass depth: use language features to remove repetition, not hide CSS
Sass can provide modules, functions, mixins, loops, and compile-time variables.
@use "tokens";
@mixin focus-ring {
outline: 3px solid tokens.$focus;
outline-offset: 2px;
}
.button:focus-visible {
@include focus-ring;
}
Do not create a mixin for every two-line declaration block. If native CSS custom properties, nesting, or functions solve the problem, prefer the simpler runtime platform.
Sass variables are resolved at build time. CSS custom properties participate in runtime cascade/inheritance. They solve different problems.
PostCSS depth: transformation pipeline
PostCSS is an ecosystem for parsing and transforming CSS. Common uses include:
- Autoprefixer based on a browser support policy;
- linting;
- minification;
- syntax transformations;
- custom build-time conventions.
The key architecture question is not “Do we use PostCSS?” but “Which transformations are in the pipeline, why, and what browser contract do they implement?”
Do not manually add prefixes just because you remember old browser bugs. Let an explicit support policy drive transformation.
CSS Modules
A CSS Module typically scopes class names to an imported module:
/* ProductCard.module.css */
.card {
border: 1px solid var(--border);
}
import styles from "./ProductCard.module.css";
element.className = styles.card;
Benefits:
- local naming;
- lower accidental global collision;
- works well with component systems.
Costs:
- still need cascade and inheritance knowledge;
- global tokens/base styles need an intentional home;
- generated names can complicate debugging if tooling is poorly configured.
CSS-in-JS: distinguish runtime and extracted approaches
“CSS-in-JS” covers different architectures.
Runtime style generation can make styles depend directly on JavaScript state but may add runtime work, framework coupling, and server-rendering complexity.
Build-time/extracted approaches can provide component-local authoring while emitting static CSS.
Do not evaluate the category only by syntax. Compare:
- runtime cost;
- server rendering/hydration;
- caching;
- critical CSS behavior;
- theming;
- type safety;
- debugging;
- team familiarity;
- framework lifecycle.
Practical decision matrix
| Need | Start with |
|---|---|
| Small/static site | plain CSS + layers |
| Reusable naming convention | BEM or equivalent component convention |
| Build-time functions/modules | Sass |
| Browser-policy transformations | PostCSS |
| Component-local class names | CSS Modules |
| State-heavy framework styling | evaluate CSS Modules, extracted CSS-in-JS, or runtime CSS-in-JS based on measured needs |
Tooling should make ownership clearer. If it makes ordinary cascade behavior impossible to explain, architecture has become weaker, not stronger.
Common mistakes
- Adopting Sass to avoid learning the cascade.
- Using nested selectors five levels deep because SCSS permits it.
- Assuming CSS Modules eliminate all global CSS concerns.
- Using CSS-in-JS for static rules that could be ordinary CSS.
- Introducing a framework before understanding the underlying CSS.
- Treating BEM as a replacement for semantic HTML.
- Letting tool configuration silently change browser support without documentation.
Practice set
- Refactor a deep selector into BEM-style component classes.
- Write the same theme value as Sass variable and CSS custom property; explain runtime differences.
- Sketch a CSS Modules component API.
- List which project requirements would justify PostCSS.
- Compare plain CSS + layers versus a preprocessor architecture for a small site.
Recap
CSS architecture is about ownership and predictable change. BEM is naming; Sass and PostCSS are build tools; CSS Modules provide local scoping; CSS-in-JS is a family of application styling approaches. Learn platform CSS first, then add tooling where it earns its complexity.
Official references
-
Sass documentation — https://sass-lang.com/documentation/
-
PostCSS — https://postcss.org/
-
roadmap.sh CSS topic: BEM / Sass / PostCSS / CSS Modules / CSS-in-JS — https://roadmap.sh/css
-
Sass documentation — https://sass-lang.com/documentation/
-
PostCSS — https://postcss.org/
-
CSS Modules repository — https://github.com/css-modules/css-modules
