Apply

Semantic naming

Names should explain ownership and intent, not describe temporary colours, layout accidents, or what someone thought looked tidy in a sprint review. Good naming reduces questions before they happen.

Core naming rules

  • Name components by purpose, not appearance.
  • Keep child classes short and scoped by the parent.
  • Use modifiers for reusable variants.
  • Use .is_* for temporary state.
  • Use .has_* for conditions and content presence.
  • Avoid utility names pretending to be components.
  • If the name needs explaining, the architecture probably does too.

Methodology influences

LSCSS borrows useful parts of older systems:

  • RSCSS influences short contextual child selectors.
  • BEM influences explicit modifier classes.
  • ITCSS influences ordered layers and predictable precedence.
  • Modern CSS adds tools such as :where(), @scope, and container queries.

Components and child selectors

Components should describe what something is, not how it currently looks. Child selectors stay short because the parent already gives them context.

CSS code example
.product-card {}
.site-header {}
.filter-panel {}

CSS code example
.product-card > .title {}
.product-card > .price {}
.site-header > .logo {}
.filter-panel > .actions {}

This keeps HTML readable without losing ownership. The selector carries structure, the class name carries meaning.

Optional component namespaces

Namespacing adds a short prefix to component roots, such asabc-product-card instead of product-card. It is optional, and each prefix should stay short — usually two to three characters tied to the product, team, or migration wave. At scale, a second area prefix can sit between that root and the component name — abc-co-basket-item whereco marks checkout. Seearea namespaces below.

The clearest win is often legacy coexistence, not greenfield tidiness. When you are updating an old system and cannot rename or remove every existing class at once, putting everynew component behind a namespace avoids collisions with inherited CSS and makes ownership obvious in markup, partials, and reviews. If templates still contain product-card besideabc-product-card, you can tell immediately which block is legacy and which is the path forward — without decoding selector specificity or guessing which stylesheet won.

CSS code example
/* Legacy — leave unchanged during migration */
.product-card {}
.site-header {}

/* New — same semantics, namespaced root */
.abc-product-card {}
.abc-site-header {}

CSS code example
.abc-product-card > .title {}
.abc-product-card > .price {}
.abc-site-header > .logo {}

Area namespaces at scale

A single short prefix is often enough. On large e-commerce codebases, though, component volume alone can make otherwise good semantic names collide — basket-item in checkout, a mini-basket in the header, and a line item on the order summary are different components with similar responsibilities. When that happens, add a second short prefix for the site area or journey the component belongs to:abc-co-basket-item — primary namespace, area code, then the component name.

The first prefix still marks product, team, or migration ownership (abc). The second marks where the component lives in the site (co for checkout, pl for product listing, ac for account, and so on). Shared components that appear across areas keep only the primary prefix —abc-product-card, notabc-co-product-card.

CSS code example
/* Shared — one namespace, used site-wide */
.abc-product-card {}
.abc-site-header {}

/* Area-specific — primary namespace + area code + component */
.abc-co-basket-item {}
.abc-co-order-summary {}
.abc-pl-line-item {}
.abc-ac-payment-method {}

/* abc = product or team prefix; co = checkout, pl = product listing, ac = account */

CSS code example
.abc-co-basket-item > .title {}
.abc-co-basket-item > .quantity {}
.abc-pl-line-item > .image {}
.abc-pl-line-item > .price {}

  • Use namespaces when collision risk is real — especially while old and new CSS live side by side during migration.
  • Namespace new work consistently during a migration so every prefixed class reads as “owned by the new system”.
  • Keep each prefix short: usually 2 to 3 characters. With two prefixes, brevity matters more — long chains defeat the point.
  • Add a second area prefix when component volume or journey boundaries make single semantic names ambiguous; document area codes the same way you document the primary namespace.
  • Keep component names semantic after the prefix(es).
  • Do not namespace by default in small isolated greenfield codebases.
  • Do not replace layers, scope, or ownership with prefixes alone.

Reasonable selector depth

Not every component can be only direct children forever. Real UI structure has wrappers such as media and content containers. Keep selector depth practical: short enough to stay maintainable, deep enough to be clear.

  • Default to root plus one or two descendants.
  • Use direct child selectors where ownership boundaries matter.
  • Use descendant selectors when the extra structure is incidental.
  • Avoid routine selectors that chain through every wrapper level.
  • If a selector feels long, introduce one stable context hook.
CSS code example
/* Good: explicit where structure matters */
.product-card > .images > .image {}
.product-card > .content > .title {}

/* Good: shorter when structure is stable enough */
.product-card .image {}
.product-card .brand {}

/* Avoid: deep chains for routine component styling */
.abc-product-card > .content > .content-actions > .wishlist-button {}

/* Prefer: keep one stable context hook, then target the control */
.abc-product-card .content-actions > .wishlist-button {}

/* Best: reduce depth wherever possible, sensible, and reliable */
.abc-product-card .wishlist-button {}

A useful rule of thumb: if you regularly need more than three steps from the component root, the markup or class architecture likely needs a clearer hook.

When @scope helps

Native @scope is worth mentioning here because it attacks the same problem from a different angle: selector length. Inside a scope rooted at .product-card, child rules can stay short (.title, .price) without repeating the component name in every selector or chaining through every wrapper. The browser enforces the boundary; you keep semantic class names in HTML.

CSS code example
@scope (.product-card) {
    .title {}
    .price {}
}

/* Same ownership without @scope — parent context in each selector */
.product-card > .title {}
.product-card > .price {}

That complements LSCSS shallow selectors — it does not replace them on every project today. Layers, modifiers, state classes, and theme rules still matter. Namespaces during legacy migration still help in markup at a glance (product-card vsabc-product-card); @scope is a CSS-side boundary, not a substitute for that signal.

Where @scope is supported and agreed for production, use it to simplify selectors inside a stable component root. Where it is not, keep the parent context in the selector list as shown above. See @scope and LSCSSfor trade-offs, and browser supportbefore relying on it site-wide.

Modifiers and state

A featured card and a loading card are different problems. One is a reusable variant. One is temporary behaviour. Do not force both into the same naming pattern. Hyphens and -- mark components and modifiers; underscores appear only inis_* and has_* names — seewhy state uses underscoreson the modifiers and state page.

CSS code example
/* Modifiers — reusable variants on the component root */
.product-card--featured {}
.product-card--compact {}

/* State — temporary behaviour; selector includes the component */
.product-card.is_loading {}
.product-card > .cta.is_disabled {}

/* Conditions — content or structure present */
.product-card.has_media {}
.product-card > .summary.has_error {}

Modifiers and state classes are short names in HTML (is_loading, has_error), but the stylesheet should still show which component owns them.product-card.is_loading, not a floating.is_loading rule that could match anywhere. Modifier roots such as .product-card--featured in this section are shown without chaining because they belong in the component partial; seemodifiers and stateand CSS nesting for when to chain or nest selectors.

This separation makes CSS easier to read and JavaScript easier to reason about.

BEM-style elements vs contextual child selectors

CSS code example
/* BEM-style element naming */
.product-card__title {}
.product-card__price {}

/* LSCSS-style contextual child selectors */
.product-card > .title {}
.product-card > .price {}

LSCSS keeps the useful modifier idea from BEM but usually avoids repeating the parent name in every child class. The parent already exists. It does not need to introduce itself at every meeting.

Names to avoid

CSS code example
/* Avoid: names describe appearance, not responsibility */
.blue-box {}
.big-red-button {}
.left-column-text {}

/* Prefer: purpose-based names that survive redesigns */
.product-card {}
.filter-panel {}
.promo-callout {}
.content-sidebar > .summary {}

Appearance-based names age badly the moment design changes and quietly become lies in production. .blue-box on a green card is the kind of thing that takes a week to notice and an afternoon to untangle. Purpose-based names hold up — usually as two-word component roots such as product-card orfilter-panel, with short child classes where the parent already provides context.