Leveraging the Atomic Design System with SCSS, ITCSS, and BEM for Effective Web DevelopmentCreating Powerful Design Systems with Atomic Design, SCSS, ITCSS, and BEM

Introduction

Every large-scale frontend project eventually confronts the same set of problems: a stylesheet that started clean gradually becomes a tangle of overrides, a component library with no consistent naming discipline, and a team of engineers who spend more time debugging specificity wars than building features. These problems are not caused by careless developers — they are caused by the absence of intentional architecture.

The combination of Atomic Design, SCSS, ITCSS, and BEM addresses this architectural vacuum. Each methodology solves a specific problem: Atomic Design gives you a mental model for composing interfaces from reusable parts; ITCSS organises your CSS into layers that respect the cascade; BEM provides a deterministic naming convention that communicates structure at a glance; and SCSS supplies the tooling — variables, mixins, functions, and partials — that makes all of it maintainable at scale. None of these methodologies is sufficient on its own, but together they form a coherent, battle-tested approach to frontend architecture that has proven itself across teams of every size.

This article explores each methodology in depth, examines how they interlock, and provides concrete implementation patterns that you can adapt to real projects. The goal is not to advocate for blind adoption of any orthodoxy, but to show you how deliberate choices about structure and naming can eliminate entire categories of frontend maintenance problems before they emerge.

The Problem: Why CSS at Scale Fails

CSS is deceptively easy to write and notoriously difficult to maintain. A single developer working on a small project can keep an entire stylesheet in their head. As soon as a second developer joins, or the project grows beyond a few hundred lines, the cognitive overhead begins to compound. Without a shared mental model, two engineers will inevitably invent conflicting naming patterns. Without a defined specificity strategy, new rules will collide with existing ones in unpredictable ways. Without a component abstraction, the same button style will be copy-pasted into a dozen different files and then diverge as each is modified independently.

The symptoms are familiar to any senior frontend engineer: !important used as a blunt instrument to force specificity wins; class names like .red-text or .new-header that encode neither purpose nor structure; entire stylesheet files that nobody dares delete because the consequences are unknown. The root cause, in almost every case, is not a knowledge deficit but an architectural deficit. The codebase grew organically without a model that could scale, and by the time the problem became obvious, the cost of refactoring felt prohibitive. Understanding this failure mode is the first step toward designing systems that avoid it.

Atomic Design: A Mental Model for UI Composition

Atomic Design, introduced by Brad Frost in his 2013 article and later expanded in his book Atomic Design (2016), borrows its taxonomy from chemistry. The premise is that user interfaces, like matter, are composed of progressively larger combinations of simpler units. Rather than thinking of a page as a monolithic thing to be styled, you think of it as an assembly of discrete, reusable parts.

The taxonomy has five levels. Atoms are the smallest indivisible UI elements: a button, an input field, a label, a colour swatch. Molecules are functional groupings of atoms: a search form composed of an input, a button, and a label. Organisms are complex, self-contained sections of the interface: a navigation bar that contains a logo atom, a search molecule, and a list of link atoms. Templates are page-level skeletons — wireframe-like arrangements of organisms that define layout without real content. Pages are instances of templates populated with real content, representing what a user actually sees.

What makes this mental model valuable for engineering is that it maps directly to how component-based frontend frameworks like React and Vue encourage you to think. Every level of the atomic hierarchy corresponds naturally to a component boundary. An atom is a primitive component, a molecule is a composed component, an organism is a feature-level component. By giving names to these levels, Atomic Design provides a shared vocabulary that designers and engineers can use during planning, code review, and refactoring — reducing the ambiguity that causes components to proliferate without purpose.

ITCSS: Organising CSS by Specificity and Scope

Inverted Triangle CSS (ITCSS), developed by Harry Roberts, solves a different problem from Atomic Design. Where Atomic Design is about how you compose UI components conceptually, ITCSS is about how you organise your CSS files so that the cascade works with you rather than against you.

The core insight behind ITCSS is that CSS specificity and scope should flow in one direction: from the most generic and least specific rules at the top of the triangle, to the most explicit and most specific rules at the bottom. The "inverted triangle" metaphor captures this: a wide top layer containing low-specificity, global rules, narrowing progressively to a pointed bottom layer containing highly specific, component-level overrides. ITCSS defines seven canonical layers: Settings (variables and configuration, no CSS output), Tools (mixins and functions, no CSS output), Generic (low-specificity resets and normalisations, such as box-sizing), Elements (bare HTML element styles with no classes), Objects (layout and structural patterns, class-based but cosmetic-free), Components (discrete UI components, the bulk of your authored CSS), and Utilities (single-purpose helper classes with the highest specificity and often !important).

The practical benefit of this structure is that it eliminates specificity surprises. When you know that utilities always come after components in the source order, and that components always come after elements, you can predict with confidence which rules will win in any given conflict. You do not need to add specificity artificially — you navigate to the correct layer instead. ITCSS is also technology-agnostic: it is a file organisation strategy, not a syntax requirement. It works equally well with plain CSS, Less, or SCSS.

BEM: A Naming Convention That Communicates Intent

Block Element Modifier (BEM) is a naming methodology developed by the Yandex frontend team and published publicly around 2010. Its central claim is simple: a class name should communicate the role, structure, and state of the element it describes, without requiring you to read the HTML or inspect the DOM. BEM achieves this through a strict double-underscore and double-hyphen syntax that encodes three concepts — Block, Element, and Modifier — directly into the class name.

A Block is a standalone, meaningful component: .card, .nav, .button. An Element is a part of a block that has no standalone meaning outside of it, written as .block__element: .card__title, .nav__item, .button__icon. A Modifier is a flag on a block or element that changes its appearance or behaviour, written as .block--modifier or .block__element--modifier: .button--primary, .card--featured, .nav__item--active. The double delimiters were chosen deliberately to allow single hyphens and underscores in block and element names without creating ambiguity. A class like .search-form__submit-button--disabled is unambiguous: search-form is the block, submit-button is the element, and disabled is the modifier.

The engineering value of BEM extends beyond cosmetics. Because block names serve as namespaces, BEM effectively eliminates the risk of unintended CSS collisions between components. A .title class inside a .card component is always .card__title — it cannot leak into a .hero component's title without an explicit, visible naming decision. This makes refactoring predictable: you can remove a block's styles with complete confidence that no other component depends on those class names. BEM also pairs exceptionally well with linting tools like Stylelint, which can enforce the naming convention automatically across a large team.

How the Methodologies Fit Together

Understanding each methodology individually is necessary but not sufficient. The real value emerges from understanding how they compose into a coherent architecture. ITCSS provides the macro-structure — the file organisation and layer ordering that governs the cascade. BEM provides the micro-structure — the naming convention that governs individual class names within those files. Atomic Design provides the conceptual model — the vocabulary that aligns how engineers and designers talk about components. SCSS provides the tooling — the syntax and features that make the structure maintainable.

The mapping between these systems is intuitive once you see it. ITCSS Settings and Tools layers correspond to SCSS variables, maps, and mixins — configuration and abstractions that produce no CSS output on their own. The ITCSS Objects layer is where you would place purely structural patterns, often described in Atomic terms as layout atoms or reusable wrappers. The ITCSS Components layer is where the majority of Atomic Design molecules, organisms, and some atoms live. SCSS partials structure the file system: each BEM block gets its own partial file, collected into a directory named for its Atomic level and imported into the appropriate ITCSS layer.

This architecture scales naturally. Adding a new component means creating a new SCSS partial in the correct directory, naming its classes using BEM, and adding an @use or @forward import to the layer's index file. No existing file is modified in a structurally significant way. The cascade remains predictable, the naming remains consistent, and the cognitive cost of onboarding a new team member is dramatically reduced because every architectural decision has been codified into a visible, learnable convention.

Implementation: Building the Architecture in Practice

Directory Structure and SCSS Partials

A production-ready implementation begins with a directory structure that mirrors the ITCSS layers and contains Atomic Design subdirectories within the Components layer. Here is a representative structure:

styles/
├── 01-settings/
│   ├── _colours.scss
│   ├── _typography.scss
│   ├── _spacing.scss
│   └── _index.scss
├── 02-tools/
│   ├── _mixins.scss
│   ├── _functions.scss
│   └── _index.scss
├── 03-generic/
│   ├── _reset.scss
│   ├── _box-sizing.scss
│   └── _index.scss
├── 04-elements/
│   ├── _headings.scss
│   ├── _links.scss
│   └── _index.scss
├── 05-objects/
│   ├── _container.scss
│   ├── _grid.scss
│   └── _index.scss
├── 06-components/
│   ├── atoms/
│   │   ├── _button.scss
│   │   ├── _input.scss
│   │   └── _badge.scss
│   ├── molecules/
│   │   ├── _search-form.scss
│   │   ├── _card.scss
│   │   └── _form-group.scss
│   ├── organisms/
│   │   ├── _site-header.scss
│   │   ├── _product-grid.scss
│   │   └── _site-footer.scss
│   └── _index.scss
├── 07-utilities/
│   ├── _spacing.scss
│   ├── _visibility.scss
│   └── _index.scss
└── main.scss

The main.scss entry point imports each layer's index in strict order:

// main.scss - Layer order must never be altered
@use '01-settings' as *;
@use '02-tools' as *;
@use '03-generic';
@use '04-elements';
@use '05-objects';
@use '06-components';
@use '07-utilities';

Settings: Design Tokens as SCSS Maps

The Settings layer is where your design tokens live. Using SCSS maps rather than flat variables allows you to express the relationships between token values and build accessor functions that enforce consistent usage.

// 01-settings/_colours.scss

$colour-palette: (
  'brand': (
    'primary':   #1a56db,
    'secondary': #0e9f6e,
    'accent':    #ff5a1f,
  ),
  'neutral': (
    '100': #f9fafb,
    '200': #f3f4f6,
    '500': #6b7280,
    '900': #111827,
  ),
  'feedback': (
    'error':   #f05252,
    'warning': #e3a008,
    'success': #0e9f6e,
  ),
) !default;

// 01-settings/_spacing.scss

$spacing-scale: (
  '0':  0,
  '1':  0.25rem,
  '2':  0.5rem,
  '3':  0.75rem,
  '4':  1rem,
  '6':  1.5rem,
  '8':  2rem,
  '12': 3rem,
  '16': 4rem,
) !default;

Tools: Reusable Mixins and Accessor Functions

The Tools layer provides the abstractions that make the rest of the codebase DRY. Accessor functions enforce that only tokens defined in the Settings maps are used — any attempt to retrieve a non-existent token throws a compile-time error.

// 02-tools/_functions.scss

/// Retrieve a colour from the design token map.
/// @param {string} $group - The colour group key (e.g., 'brand', 'neutral')
/// @param {string} $key   - The colour key within the group
/// @return {color}
@function colour($group, $key) {
  $group-map: map-get($colour-palette, $group);

  @if not $group-map {
    @error "Colour group '#{$group}' does not exist in $colour-palette.";
  }

  $value: map-get($group-map, $key);

  @if not $value {
    @error "Colour key '#{$key}' does not exist in group '#{$group}'.";
  }

  @return $value;
}

/// Retrieve a spacing value from the design token scale.
/// @param {string} $step - The spacing scale step (e.g., '4', '8')
/// @return {length}
@function space($step) {
  $value: map-get($spacing-scale, '#{$step}');

  @if $value == null {
    @error "Spacing step '#{$step}' does not exist in $spacing-scale.";
  }

  @return $value;
}

// 02-tools/_mixins.scss

/// Apply responsive styles for a named breakpoint and above.
$breakpoints: (
  'sm': 640px,
  'md': 768px,
  'lg': 1024px,
  'xl': 1280px,
) !default;

@mixin respond-to($breakpoint) {
  $bp: map-get($breakpoints, $breakpoint);

  @if not $bp {
    @error "Breakpoint '#{$breakpoint}' is not defined.";
  }

  @media (min-width: $bp) {
    @content;
  }
}

/// Visually hide an element while keeping it accessible to screen readers.
@mixin visually-hidden {
  position:   absolute;
  width:      1px;
  height:     1px;
  padding:    0;
  margin:     -1px;
  overflow:   hidden;
  clip:       rect(0, 0, 0, 0);
  white-space: nowrap;
  border:     0;
}

Components: Atoms with BEM

With the foundational layers in place, component authoring becomes remarkably consistent. Each component file defines exactly one BEM block and imports nothing — all tokens and tools are available via the main.scss entry point's @use ... as * declarations.

// 06-components/atoms/_button.scss

.button {
  display:          inline-flex;
  align-items:      center;
  justify-content:  center;
  gap:              space('2');
  padding:          space('2') space('4');
  border:           2px solid transparent;
  border-radius:    0.375rem;
  font-family:      inherit;
  font-size:        0.875rem;
  font-weight:      600;
  line-height:      1.25;
  cursor:           pointer;
  transition:       background-color 150ms ease, border-color 150ms ease, color 150ms ease;
  text-decoration:  none;
  white-space:      nowrap;

  // Default variant (secondary/ghost style)
  background-color: transparent;
  border-color:     colour('neutral', '500');
  color:            colour('neutral', '900');

  &:hover {
    background-color: colour('neutral', '200');
  }

  &:focus-visible {
    outline:        2px solid colour('brand', 'primary');
    outline-offset: 2px;
  }

  // -------------------------
  // Modifiers
  // -------------------------

  &--primary {
    background-color: colour('brand', 'primary');
    border-color:     colour('brand', 'primary');
    color:            colour('neutral', '100');

    &:hover {
      background-color: darken(colour('brand', 'primary'), 10%);
      border-color:     darken(colour('brand', 'primary'), 10%);
    }
  }

  &--danger {
    background-color: colour('feedback', 'error');
    border-color:     colour('feedback', 'error');
    color:            colour('neutral', '100');
  }

  &--small {
    padding:     space('1') space('3');
    font-size:   0.75rem;
  }

  &--large {
    padding:     space('3') space('6');
    font-size:   1rem;
  }

  &--disabled,
  &[disabled] {
    opacity:        0.5;
    cursor:         not-allowed;
    pointer-events: none;
  }

  &--full-width {
    width: 100%;
  }

  // -------------------------
  // Elements
  // -------------------------

  &__icon {
    display:     inline-flex;
    flex-shrink: 0;
    width:       1em;
    height:      1em;
  }

  &__label {
    @include visually-hidden;
  }
}

Molecules: Composing Atoms into Functional Groups

A molecule composes multiple atoms into a unit that performs a specific function. The molecule's BEM block is its own namespace; it should not reach into atom class names to override styles. If an atom needs to be styled differently within a molecule, the molecule provides a wrapper element that adjusts layout, and the atom retains its own visual integrity.

// 06-components/molecules/_search-form.scss

.search-form {
  display:     flex;
  align-items: stretch;
  width:       100%;
  max-width:   32rem;

  &__input-wrapper {
    flex:         1;
    position:     relative;
    min-width:    0; // Prevent flex overflow
  }

  &__input {
    width:            100%;
    height:           100%;
    padding:          space('2') space('4');
    padding-right:    space('10');
    border:           1px solid colour('neutral', '200');
    border-right:     none;
    border-radius:    0.375rem 0 0 0.375rem;
    font-size:        0.875rem;
    color:            colour('neutral', '900');
    background-color: colour('neutral', '100');
    transition:       border-color 150ms ease;

    &:focus {
      outline:      none;
      border-color: colour('brand', 'primary');
    }

    &::placeholder {
      color: colour('neutral', '500');
    }
  }

  &__clear-button {
    position:  absolute;
    right:     space('2');
    top:       50%;
    transform: translateY(-50%);
  }

  &__submit {
    flex-shrink:   0;
    border-radius: 0 0.375rem 0.375rem 0;
  }

  // Modifier: compact layout for use in navigation bars
  &--compact {
    max-width: 20rem;
  }
}

Organisms: Self-Contained Feature Sections

Organisms compose molecules and atoms into a self-contained section of the UI. They are the level at which concerns like layout grid, background treatment, and responsive behaviour are typically introduced. The organism's BEM block acts as a layout container; it delegates visual responsibility to its child molecules and atoms.

// 06-components/organisms/_site-header.scss

.site-header {
  position:         sticky;
  top:              0;
  z-index:          100;
  background-color: colour('neutral', '100');
  border-bottom:    1px solid colour('neutral', '200');

  &__inner {
    display:         flex;
    align-items:     center;
    justify-content: space-between;
    gap:             space('4');
    max-width:       80rem;
    margin-inline:   auto;
    padding-inline:  space('4');
    height:          4rem;

    @include respond-to('lg') {
      padding-inline: space('8');
    }
  }

  &__logo {
    flex-shrink:  0;
    display:      block;
    height:       2rem;
    width:        auto;
  }

  &__nav {
    display: none;

    @include respond-to('md') {
      display: flex;
      align-items: center;
      gap: space('1');
    }
  }

  &__search {
    flex: 1;
    max-width: 24rem;
    display: none;

    @include respond-to('lg') {
      display: block;
    }
  }

  &__actions {
    display:     flex;
    align-items: center;
    gap:         space('2');
  }

  // Modifier: transparent header for hero sections
  &--transparent {
    background-color: transparent;
    border-bottom:    none;
  }
}

Trade-offs and Pitfalls

No architecture is free from trade-offs, and adopting this combined methodology introduces real costs alongside its benefits. The most immediate is verbosity. BEM class names can become long — .site-header__nav-item--active is far more typing than .active, and it can feel cumbersome in HTML templates, especially when using templating engines that do not support dynamic class composition elegantly. Teams should invest in tooling — PostHTML, React's classnames library, or Vue's dynamic class bindings — to reduce the authoring burden without sacrificing the naming discipline.

The ITCSS layer structure can also create a false sense of completeness. Teams sometimes debate at length whether a component belongs in Objects or Components, or whether a particular helper should be a utility or a modifier. These debates are often a sign that the layer boundaries are being treated as strict categorical rules rather than pragmatic guidelines. The goal is consistent direction of specificity flow, not taxonomic purity. A pragmatic team will establish a brief decision record for ambiguous cases and move on. Spending engineering cycles on classification debates undermines the time saved by having the architecture at all.

A subtler pitfall is the temptation to reach across component boundaries — to style a .button differently by targeting it inside a .card using .card .button { ... }. This pattern, sometimes called "contextual overrides," defeats the predictability that BEM and ITCSS provide. If a button inside a card genuinely needs a different appearance, the correct solution is a button modifier (.button--card-action) or a wrapper element in the card molecule. Enforcing this discipline consistently requires code review culture and Stylelint rules as much as it requires individual understanding.

Finally, the initial setup cost is non-trivial. Establishing the directory structure, configuring SCSS module resolution, setting up Stylelint with BEM enforcement rules, and documenting the conventions for a new team member takes meaningful time. This investment is amortised over the life of a large project but may not be justified for a small, short-lived codebase. The architecture should be adopted with clear-eyed understanding of where it pays dividends and where it imposes unnecessary overhead.

Best Practices

Enforce the Architecture Programmatically

Conventions that live only in documentation are conventions waiting to be broken. Use Stylelint with the stylelint-selector-bem-pattern plugin to enforce BEM naming automatically. Configure import order linting to prevent layers from importing each other in wrong order. Add these checks to your CI pipeline so that architecture violations are caught at code review time, not discovered months later during a refactoring sprint.

// .stylelintrc
{
  "plugins": ["stylelint-selector-bem-pattern"],
  "rules": {
    "plugin/selector-bem-pattern": {
      "componentName": "[A-Z][a-zA-Z]+",
      "componentSelectors": {
        "initial": "^\\.{componentName}(?:__[a-z][a-z0-9-]*)?(?:--[a-z][a-z0-9-]*)?$"
      }
    },
    "no-descending-specificity": true,
    "selector-max-id": 0,
    "selector-max-type": [1, { "ignoreTypes": ["html", "body"] }]
  }
}

Treat Design Tokens as the Source of Truth

Every colour, spacing value, type scale step, and breakpoint that appears in your SCSS should be retrieved through a token accessor function, never hardcoded. This single discipline makes global reskin operations trivial — you update the Settings layer, and every downstream usage updates automatically. It also creates a natural audit trail: if a value appears in your output CSS that is not defined in your Settings maps, you know a convention has been broken.

Document Component APIs Explicitly

Every component partial should begin with a comment block that documents its BEM block name, its available modifiers, its expected context (is it used inside a particular organism or is it free-standing?), and any dependencies on design tokens. This documentation is the component's API contract. As the team grows, this contract prevents the slow accumulation of undocumented modifiers and contextual special-cases that eventually make components impossible to reason about.

// 06-components/atoms/_button.scss
//
// Button
// ------
// Block:     .button
// Elements:  .button__icon, .button__label (visually hidden text for icon-only buttons)
// Modifiers: .button--primary, .button--danger, .button--small, .button--large,
//            .button--full-width, .button--disabled
//
// Usage:     Free-standing. Used within .search-form, .modal, and .site-header.
// Tokens:    colour(), space() from 01-settings

Keep the Objects Layer Genuinely Cosmetic-Free

The Objects layer is frequently misused as an overflow location for components that "don't feel important enough" to be in the Components layer. The correct test for the Objects layer is strict: a class belongs there if and only if it can be used across multiple unrelated components without carrying any visual opinions. A .o-container that sets max-width and horizontal centering is an object. A .o-card-grid that adds a background colour is not — the colour is a visual opinion that should live in a component.

Key Takeaways

Five practical steps you can apply to an existing or new project immediately:

1. Audit your current specificity landscape. Before introducing ITCSS, run a specificity graph on your existing CSS using a tool like Parker or css-analyzer. Identify the layers that already exist implicitly and map them to the ITCSS layer model. This prevents the mistake of building a new architecture alongside an unreformed legacy one.

2. Start with tokens before components. Define your colour palette, spacing scale, and type scale in the Settings layer first. Writing component styles against concrete token values from day one prevents the proliferation of hardcoded magic numbers that make later token extraction painful.

3. Adopt BEM naming for new files first. In an existing project, adopt BEM naming for every new component file you create rather than attempting a big-bang rename of existing classes. Over time, the new components will dominate, and legacy names can be migrated incrementally.

4. Write a one-page architecture decision record. Document in one page: the ITCSS layer definitions for your project, the BEM naming rules, where Atomic Design levels map to directories, and the accessor function signatures. Give every new team member this document before they write their first line of CSS.

5. Add specificity linting to CI before you add any new components. The Stylelint rules that enforce BEM naming and prevent ID selectors are most valuable when applied from the beginning. Adding them later means confronting a backlog of violations; adding them first means the codebase stays clean by default.

80/20 Insight

If you implement only one thing from this entire methodology, implement the Settings layer with token accessor functions. A robust token system — one where every colour and spacing value is retrieved through a function that throws an error for undefined tokens — produces roughly 80% of the long-term maintainability benefit of the full architecture. It prevents magic numbers from accumulating, makes global reskins trivial, and forces a conversation about design consistency that benefits the entire team. The full ITCSS structure, BEM naming, and Atomic vocabulary layer on top to achieve the remaining 20% — but the token system is the foundation without which the rest is fragile.

Analogies and Mental Models

The relationship between these four methodologies is analogous to the construction of a large building. Design tokens (SCSS Settings) are the catalogue of approved materials — specific grades of steel, specific mixes of concrete, specific pane sizes of glass. Every structural decision references this catalogue rather than specifying raw dimensions independently. ITCSS layers are the building codes that specify what goes where — the foundation must be poured before the frame is erected, the electrical must run before the drywall is hung. The code does not care about aesthetics; it cares about structural integrity and the order of operations. BEM naming is the labelling system used on every component in the building — every door, duct, and circuit is tagged with a name that tells maintenance engineers what it is, what system it belongs to, and what its variant is. Atomic Design is the architect's vocabulary — the shared language that lets the structural engineer, the interior designer, and the project manager discuss a "load-bearing column" or a "modular kitchen unit" without misunderstanding.

No single metaphor captures everything, but this one illustrates why the methodologies are complementary rather than competitive: they operate at different levels of abstraction, solving different classes of problem, and together they produce a codebase that is as legible to a new contributor as a well-documented building is to a new facilities engineer.

Conclusion

The combination of Atomic Design, SCSS, ITCSS, and BEM is not a silver bullet, and it requires real investment to implement well. The initial setup cost is non-trivial, the verbosity of BEM class names requires tooling support, and enforcing the architecture across a team requires both linting automation and code review discipline. None of this is free.

What you get in return is a codebase where the cascade is predictable, component names communicate intent without requiring context, design tokens serve as the single source of truth for visual decisions, and the cost of adding a new component is low and constant regardless of how large the codebase has grown. For any project expected to outlive its first sprint and involve more than one engineer, these properties compound into an enormous reduction in maintenance overhead over time.

The best architecture is one that makes the right choice the easy choice. When your team's default behaviour — creating a new file, naming a new class, retrieving a colour value — produces well-structured, consistent, maintainable code, the architecture is working. The combination of Atomic Design, SCSS, ITCSS, and BEM, applied with pragmatism and enforced with tooling, can make that default behaviour routine.

References