skip to content

CSS Architecture

How you keep a stylesheet maintainable once it outgrows a single file: managing specificity deliberately, naming things consistently, and scoping styles so a change stays local. Interviewers ask this to gauge whether you have worked on a codebase big enough to hurt.

part ofCSSoverview, primer and where to startread it →
on this pageshow

explore

questions

27

In the BEM CSS naming methodology, what do "block", "element" and "modifier" mean, and how does each one appear in a class name?

level: juniorimportance: must knowfreq 72%

answer

  1. component, part, variant
  2. the name carries the scope
  3. double underscore joins block to part
  4. double hyphen marks the variant
  5. modifier rides beside the base class

basics

~10 s

BEM names classes after a block (a standalone component, .card), its elements (parts that exist only inside it, .card__title), and modifiers (variant flags, .card--featured). Two underscores mark an element, two hyphens mark a modifier.

solid answer

~40 s

BEM stands for Block, Element, Modifier, and it encodes a component's scope directly in the class name because CSS itself has no scoping. A **block** is a standalone, meaningful piece of UI named for what it is, not how it looks: `.card`, `.site-search`. An **element** is a part that has no meaning outside its block, written `block__element` with two underscores: `.card__title`. A **modifier** is a flag that changes a block's or element's appearance or state, written with two hyphens: `.card--featured`, `.card__title--truncated`; it is added *alongside* the base class, never instead of it. Every rule is then a single class selector, so specificity is flat and source order decides conflicts. The element part stays flat no matter how deep the markup nests — `.card__title` even if the heading sits three `div`s down.

code

css · 5 lines
css
.card { display: grid; gap: 0.5rem; }
.card__title { font-size: 1.25rem; }
.card__meta { color: #666; }
.card--featured { outline: 2px solid #b30; }
.card__title--truncated { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

go deeper

for a junior

Be able to spell the three parts confidently — block, block__element, block--modifier — and say why the prefix exists at all: CSS has one global namespace, so the name carries the scope.

for a middle

Explain the mechanics: element names are flat regardless of markup depth, every rule is a single class so specificity stays equal, and a modifier holds only the delta from the base rule.

for a senior

Show judgment about where the boundaries fall — when a part earns promotion to its own block, why components must not own their outer geometry, and how mixes keep them context-free.

for a principal

Own the enforcement question: a convention nothing checks decays, so be ready to discuss linting the class pattern, keeping one spelling across teams, and what the convention costs in rename churn.

## Why a naming convention exists at all CSS has exactly one global namespace. A selector written anywhere matches anywhere in the document, so two engineers who both write `.title` in two different stylesheets silently fight over the same elements. BEM — Block, Element, Modifier, originally from Yandex — answers that by putting the scope into the name itself: the class *is* the namespace. The whole methodology is a grammar for class names plus one structural rule (one class per rule). ## Block A block is a standalone, meaningful chunk of interface that would still make sense if you moved it somewhere else on the page: `.card`, `.menu`, `.site-search`, `.pagination`. Two naming disciplines matter here. First, name it for **what it is**, never for how it looks or where it sits — `.card`, not `.blue-box` and not `.sidebar-thing`, because the colour will change and the sidebar will move. Second, a block should not set its own external geometry (outer `margin`, `position`, a `width` that only fits one slot), because that hard-codes one context into a component meant to be reusable. ## Element An element is a part of a block that has no standalone meaning — a card's title, a menu's item, a form's submit row. It is written with the block name, two underscores, then the element name: ```css .card { } .card__media { } .card__title { } ``` Two things surprise newcomers. The element name is **flat**: it names ownership, not DOM depth, so a heading nested three levels inside the card is still `.card__title`, not a path through its ancestors. And the class is used as a **single class selector** — you write `.card__title { }`, not `.card .card__title { }`; the prefix has already done the scoping work that a descendant selector would otherwise do. ## Modifier A modifier is a flag that changes appearance, state or behaviour of the block or element it hangs off: `.card--featured`, `.button--ghost`, `.card__title--truncated`. It is applied as an extra class next to the base one: ```html <article class="card card--featured"> <h2 class="card__title">Release notes</h2> </article> ``` The modifier rule carries only the *difference* from the base, which is why both classes must be present. ## The two spellings The original Yandex syntax marks modifiers with a single underscore and optionally a value: `block__elem_mod_val`. The variant most teams use today — popularised by the getbem.com write-up — is the "two dashes" style: `block__elem--mod`. Both are legitimate BEM; what is not legitimate is mixing them in one codebase, because the delimiters are the only thing telling a reader which part of the name is which. ## What the grammar actually buys - **Readability from the markup alone.** Seeing `class="card__footer"` in a template tells you which stylesheet component owns it, without grepping. - **Flat specificity by construction.** Every selector is one class, so they all weigh the same and conflicts resolve by source order rather than by who nested deeper. - **Safe deletion.** Removing a component means removing one name prefix; nothing else in the sheet claims those names. - **Visible misuse.** A `card__title` appearing outside a `card` is an obvious defect to a reviewer, which a generic `.title` never would be. ## Mixes: putting a block inside another block Because blocks must not own their outer positioning, BEM uses a **mix** — two entities on one DOM node: ```html <ul class="feed"> <li class="feed__item"> <article class="card">…</article> </li> </ul> ``` or, collapsing the node, `<article class="card feed__item">`. The `card` class supplies the component; the `feed__item` class supplies the parent's layout slot. The card stays context-free and reusable. ## The mistakes reviewers look for Chaining element names (`card__body__title`); naming blocks after appearance; applying a modifier without its base class; and re-introducing descendant selectors such as `.card .card__title`, which throws away the flat specificity the naming already guaranteed.

  • How do you decide whether something is an element of a block or a block in its own right?
    Ask whether it means anything outside its parent. A card's footer is meaningless alone, so it is `card__footer`. An avatar, a button or a badge is reusable elsewhere, so it is its own block — and where it sits inside a card, you mix the two classes on one node (`class="avatar card__avatar"`) so the card owns the placement and the avatar owns itself.
  • Is the two-dashes syntax the only correct BEM?
    No. The original Yandex convention marks modifiers with a single underscore and supports key/value modifiers, as in `menu__item_state_active`. The two-dashes form `menu__item--active` is a later, more widely adopted variant. Either is valid BEM; mixing both in one codebase is not, because the delimiters are the only signal of which part of the name is which.
  • Why does BEM insist a block should not set its own margin or position?
    Because those properties describe a relationship to a surrounding context, not the component. Baking them in means the block only fits one place. BEM pushes outer geometry onto the parent's element class — the mix pattern — so the same block drops into a sidebar, a grid cell or a modal without overrides.

Think of it as a filesystem path flattened into one word: the block is the folder, the element is the file inside it, and the modifier is a flag you pass alongside it rather than a different file.

saying these in an interview costs you the question

  • Says the element class only works nested under the block selector
  • Names blocks by appearance, like .blue-box or .left-column
  • Applies a modifier class alone, without the base class
  • Chains element names to mirror how deep the markup nests
  • Treats block, element and modifier delimiters as interchangeable

context

open as a page

In CSS, why is a class rule such as .title { color: red } described as living in a global namespace, and what problems does that create in a large codebase?

level: juniorimportance: must knowfreq 55%

basics

~20 s

CSS selectors are matched against the entire document regardless of which file they were written in, so one .title rule styles every element with that class. Collisions are silent, and unrelated components end up restyling each other.

open as a page

Most CSS style guides tell you to style a component with a single class like `.card__title` instead of a chain like `#main .card h2`. Both match the same heading — what does the flat single-class convention buy a growing codebase?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A single class keeps every rule at the same low weight, so a later rule can override it by source order alone. IDs and long descendant chains raise the weight, forcing every future override to be heavier still.

open as a page

In CSS architecture, what does a "utility-first" approach mean compared with writing semantic component classes, and what trade do the two make against each other?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Utility-first composes many single-purpose classes in the markup, each setting one or two declarations. The semantic approach puts one meaning-based class on the element and defines all its styling in the stylesheet. The trade is markup noise versus an ever-growing stylesheet.

open as a page

In a codebase where each UI component ships with its own stylesheet file, which kinds of CSS legitimately belong in the shared global stylesheet instead, and which do not?

level: middleimportance: must knowfreq 62%

basics

~20 s

Global CSS holds what must exist exactly once for the whole document: the reset, @font-face declarations, custom-property definitions on :root, base element typography, and a small utility set. Anything describing one component belongs in that component's file.

open as a page

On a large team, CSS overrides have escalated until many declarations end in `!important`. What drives that escalation, and what change in how rules are authored actually stops it?

level: middleimportance: must knowfreq 55%

basics

~20 s

Nothing in CSS lowers an existing rule's weight, so the cheapest way to override is always to write something heavier, and the floor keeps rising until it hits !important. The fix is authoring every rule at the same low weight and deciding conflicts by a defined order instead.

open as a page

In CSS, what is the difference between a reset stylesheet and a normalize stylesheet, and where in a project's stylesheet order does it belong?

level: juniorimportance: should knowfreq 55%

basics

~20 s

A reset strips browser defaults — margins, list markers, heading sizes — down to a flat baseline. A normalize stylesheet keeps useful defaults and only patches cross-browser inconsistencies. Either one loads first, before base and component rules.

open as a page

In BEM, why is a class such as `card__body__title` considered wrong for an element nested inside another element, and how should deeply nested markup be named instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

BEM has only two levels: a block and a flat set of elements it owns. Element names describe ownership, not DOM depth, so the class stays card__title however deep the node sits. If a part needs its own parts, promote it to a block.

open as a page

In BEM, a modifier is written as a second class on the node (`class="btn btn--primary"`) rather than replacing the base class. Why, and what does that imply about how the two rules interact?

level: middleimportance: should knowfreq 45%

basics

~20 s

A modifier rule holds only the difference from the base, so the base class must stay on the node to supply the shared styles. Both are single-class selectors of equal specificity, so the modifier must come later in the stylesheet to win.

open as a page

What are OOCSS's two principles — separating structure from skin, and container from content — and how do they show up in the class names you write?

level: middleimportance: should knowfreq 32%

basics

~20 s

OOCSS, from Nicole Sullivan, says to split a component's box mechanics (structure) from its paint (skin) so a new variant costs one small rule, and to style objects by their own class rather than by where they sit, so the same class works anywhere.

open as a page

In a CSS file the browser loads directly with a <link> tag, what rules govern where the @import at-rule may appear, and what does it cost at runtime compared with composing the same files at build time?

level: middleimportance: should knowfreq 45%

basics

~20 s

CSS @import rules must appear before any style rule — only @charset and @layer statements may precede them — and a misplaced one is silently ignored. At runtime each import is discovered only after its parent sheet is parsed, so requests serialize; a build step inlines them instead.

open as a page

In CSS, what does the optional to (...) limit on the @scope at-rule do, and why is a rule such as @scope (.card) to (.card-body) called donut scoping?

level: middleimportance: should knowfreq 30%

basics

~20 s

The to (...) selector marks a lower boundary: rules inside the @scope block match the scope root and its descendants, but stop at any element matching the limit, that element included. The styled region is a subtree with a hole, hence donut.

open as a page

A utility-first CSS system exposes only a fixed scale of spacing, size and colour classes. What does that closed vocabulary actually buy compared with writing free-form declarations in a component stylesheet, and what does it cost?

level: middleimportance: should knowfreq 46%

basics

~20 s

A closed utility vocabulary makes off-scale values inexpressible, so consistency is enforced by the system rather than by review discipline. The cost is friction whenever a legitimate design genuinely needs a value the scale does not contain.

open as a page

Utility-first CSS is often accused of bloat because elements carry long lists of tiny classes. Compare how the shipped CSS actually scales in a utility-first codebase versus one built from semantic component classes, and say where each really pays its cost.

level: middleimportance: should knowfreq 40%

basics

~20 s

Utility CSS scales with the number of distinct utilities used, so it plateaus as a codebase grows. Component-class CSS scales with the number of components, so it grows roughly linearly and rarely shrinks. Utility-first shifts the cost from the stylesheet into markup bytes.

open as a page

What are the practical costs of adopting BEM naming across a large CSS codebase, and what would make you conclude it is no longer paying for itself?

level: seniorimportance: should knowfreq 40%

basics

~20 s

BEM's costs are verbose markup, long repetitive names, and rename churn across templates, stylesheets, tests and scripts. Its bigger weakness is that nothing enforces it: one descendant selector reintroduces the coupling it removed. It stops paying when discipline cannot be sustained or tooling already guarantees scoping.

open as a page

You inherit a 12,000-line stylesheet and want to delete the rules nothing uses. How do you identify dead CSS, and why do coverage reports and automated purge tools over-report rules as unused?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Nothing in CSS errors on a selector that matches nothing, so deletion is evidence-gathering, not compilation. Coverage tools report only what a recorded session exercised, and purge tools match class-name strings in source, so states, breakpoints and dynamically built class names all look dead when they are not.

open as a page

You inherit a CSS codebase full of selectors like `#app .sidebar ul li a.active`. How do you get specificity down across it incrementally, without a rewrite and without visually breaking pages?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Do not lower the old rules first. Contain them — move the legacy stylesheet into a low cascade layer, or neutralise chains with :where() — so new flat rules win without escalating, then migrate component by component behind visual checks.

open as a page

You maintain a shared CSS component library used by other teams. How do you author its default rules so a consumer's own single-class rule can override them, without them resorting to `!important` or copying your selector structure?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Ship defaults at zero or near-zero cascade weight: wrap the library's selectors in :where() so they contribute nothing to specificity, or publish the library inside a low cascade layer. Either way a consumer's plain class wins without escalating.

open as a page

In a large CSS codebase, changing one element's utility classes feels safe while changing `.card { padding: 24px }` in the stylesheet does not. Explain the mechanism behind that asymmetry and what it means for a codebase maintained over several years.

level: seniorimportance: should knowfreq 54%

basics

~20 s

A shared class is a global name with an unknown set of users, so editing it has unbounded blast radius. Utility classes on one element affect only that element. Over years, unbounded blast radius makes engineers add rules instead of changing them, and stylesheets become append-only.

open as a page

You find the same long string of utility classes repeated on a dozen elements across a codebase. What signals tell you it is time to extract a component, and what are your options for doing that extraction?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Extract when the repetitions must change together — the repeated string represents one concept, not a coincidence. Prefer extracting the markup into a reusable template or component so the class string exists once; extracting a CSS class instead reintroduces the shared-rule blast radius.

open as a page

You own frontend standards for an app whose global stylesheet grows every sprint and never shrinks. Which structural conventions and enforcement mechanisms actually stop that decay, and what does each one cost?

level: principalimportance: should knowfreq 30%

basics

~20 s

Stylesheets grow because adding a rule is always cheaper than understanding the existing ones, and nothing ever fails when a rule goes unused. The fixes are structural: colocate CSS with what it styles, keep the global layer a closed set with an owner, allow raw values only in the token file, and enforce it in CI rather than in review.

open as a page

For a CSS component library shared by several teams, how would you choose between @scope, build-time class-name hashing, and shadow DOM as the style-encapsulation strategy, and what does each actually guarantee?

level: principalimportance: should knowfreq 26%

basics

~20 s

Hashing guarantees your class names cannot collide with anyone else's. @scope guarantees your rules only match inside a chosen subtree. Only shadow DOM guarantees outside selectors cannot reach in — and none of them stop inherited properties from crossing.

open as a page

You own a design system's CSS consumed by several product teams. How would you define and enforce the contract for how product code is allowed to override it, and what breaks if you leave that contract implicit?

level: principalimportance: should knowfreq 28%

basics

~20 s

Publish an explicit override mechanism — a declared cascade-layer order with a layer reserved for product code, zero-weight defaults, and custom properties for values meant to be configured — and enforce it with linting. Left implicit, teams discover it by escalating selectors, and the escalation is permanent.

open as a page

You lead frontend for a long-lived product with several teams contributing. How would you decide between a utility-first system and semantic component classes, and where would you draw the boundary if you allowed both?

level: principalimportance: should knowfreq 32%

basics

~20 s

Decide on the codebase's shape, not taste: utility-first pays off only where a componentized markup layer already collapses duplication. If both are allowed, split by responsibility — utilities compose inside components, hand-written CSS owns base layers, structural features and third-party overrides — and enforce the boundary explicitly.

open as a page

What are the five categories SMACSS sorts CSS rules into, and what do namespace prefixes such as `l-`, `is-`, `c-` and `u-` tell a reader about a class?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

SMACSS sorts rules into Base, Layout, Module, State and Theme, prefixing layout with l- and states with is- or has-. Namespace prefixes generalise the idea: c- marks a component, o- an abstract object, u- a utility, so the prefix advertises how risky a class is to edit.

open as a page

Inside a CSS @scope block, what does the :scope pseudo-class match, and how does a bare selector such as img behave differently from :scope img?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Inside an @scope block, :scope matches the scope root element itself. Bare selectors are implicitly prefixed with :scope and a descendant combinator, so img means :scope img and can never match the root — only elements beneath it.

open as a page

In the CSS cascade, what is scope proximity, at what point is it consulted, and what does it decide?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

Scope proximity is a cascade tiebreaker for declarations coming from @scope blocks: the one whose scoping root is fewer generational hops from the matched element wins. It is consulted after specificity and before order of appearance.

open as a page