Module: CSS
CSS·039·6 MIN READ

039: CSS Architecture and Tooling — BEM, Sass, PostCSS, CSS Modules, and CSS-in-JS

TOPICS COVERED: 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:

css
/* 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.

html
<article class="card card--featured">
  <h2 class="card__title">Pro plan</h2>
  <p class="card__price">$29</p>
</article>
css
.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:

css
.pricing section article > div h3 {
  color: purple;
}

Stable:

css
.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:

scss
$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:

scss
$brand: #2563eb;

resolved at build time.

CSS:

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:

css
/* 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:

jsx
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:

jsx
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:

css
:root {
  --brand: #2563eb;
}

Native nesting:

css
.card {
  padding: 1rem;

  & .card__title {
    font-weight: 700;
  }
}

Cascade layers:

css
@layer reset, base, components, utilities;

Container queries:

css
@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

css
@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:

  1. reset/normalization;
  2. design tokens;
  3. element defaults;
  4. layout primitives;
  5. components;
  6. utilities;
  7. narrowly scoped overrides.

Cascade layers can make that order explicit:

css
@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:

css
.dashboard .right-column .orders .card h3 {
  ...
}

Stronger:

css
.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.

scss
@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:

css
/* ProductCard.module.css */
.card {
  border: 1px solid var(--border);
}
js
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

NeedStart with
Small/static siteplain CSS + layers
Reusable naming conventionBEM or equivalent component convention
Build-time functions/modulesSass
Browser-policy transformationsPostCSS
Component-local class namesCSS Modules
State-heavy framework stylingevaluate 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

  1. Refactor a deep selector into BEM-style component classes.
  2. Write the same theme value as Sass variable and CSS custom property; explain runtime differences.
  3. Sketch a CSS Modules component API.
  4. List which project requirements would justify PostCSS.
  5. 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