Skip to content
academia.sh

Lesson 18 / 23

Naming Methodologies

The block-element-modifier approach; keeping specificity flat, a name announcing ownership, the separation between modifier and state, and the pattern being checkable.

Contents

Three stylesheets have accumulated in this course. Visual declarations in station.css, layout in layout.css, motion in motion.css. Class names have multiplied too: .card, .heading, .wide, .summary, .indicator.

None of these names say who they belong to. Is .heading the measurement card’s heading, or the aside’s? Is it safe to use the same name when a new section is added to the page? What else breaks when a rule is deleted?

This topic ties the answer to these questions to an architecture. This lesson takes the first step: the names themselves.

What Naming Solves

As a stylesheet grows, three problems appear.

Ownership. If which component a class name belongs to cannot be read from the name itself, no one can change that name with confidence. The place to make the change cannot be found.

Specificity creep. One more class gets added to a selector to override a declaration; then one more to override that. The triple defined in the Cascade and Specificity lesson starts growing, and !important eventually gets written.

Deletability. If which part of the document a rule matches is unknown, that rule cannot be deleted. The stylesheet only grows.

A naming methodology takes on all three with a single decision: a name is derived not from the element’s place in the document, but from the component it belongs to.

Block, Element, Modifier

The common methodology splits a name into three parts.

A block is a component that is meaningful and portable on its own. Wherever it is placed on the page, it is the same thing: the measurement card, the navigation bar, the measurement filter.

An element is part of the block and has no meaning without it: the card’s heading, the card’s value, the card’s unit. The name starts with the block’s name and is separated by two underscores: measurement-card__heading.

A modifier is a variant of the block or the element: a wide card, a warning card, a short heading. It is separated by two hyphens: measurement-card--wide.

One term distinction is needed: here, element means a part of a block. Nodes in the HTML document are called elements in this and earlier courses too. The two words name different things; measurement-card__heading is an element name and can be written on an h3 element.

Keeping Specificity Flat

The methodology’s critical consequence is that every rule has a single class selector. The program below computes selectors’ specificity triples and compares the distribution of two notations.

// naming.mjs — checking selector specificity and name patterns
// Specificity triple (a, b, c): a=id, b=class/attribute/pseudo-class, c=type/pseudo-element

// :where(...) content adds no specificity; the most specific selector inside :is(...) counts.
function specificity(selector) {
  let s = selector;
  let extra = [0, 0, 0];
  s = s.replace(/:where\([^()]*\)/g, " ");                       // no contribution
  s = s.replace(/:is\(([^()]*)\)/g, (_, inner) => {              // most specific branch counts
    const branches = inner.split(",").map((d) => specificity(d.trim()));
    branches.sort((x, y) => y[0] - x[0] || y[1] - x[1] || y[2] - x[2]);
    extra = extra.map((v, i) => v + branches[0][i]);
    return " ";
  });
  const a = (s.match(/#[\w-]+/g) || []).length;
  const b = (s.match(/\.[\w-]+/g) || []).length
          + (s.match(/\[[^\]]+\]/g) || []).length
          + (s.match(/:(?!:)[\w-]+(\([^()]*\))?/g) || []).length;
  const c = (s.match(/::[\w-]+/g) || []).length
          + (s.replace(/::[\w-]+/g, " ").replace(/:(?!:)[\w-]+(\([^()]*\))?/g, " ")
             .match(/(^|[\s>+~(])([a-zA-Z][\w-]*)/g) || []).length;
  return [a + extra[0], b + extra[1], c + extra[2]];
}

const SELECTORS = [
  "#content .measurements-section ul li a.active",
  ".station-layout .measurements-section .measurement-cards .card .heading",
  "section.measurements-section > ul > li",
  ".measurement-card__heading",
  ".measurement-card--warning",
  ".measurement-card:hover",
  ".measurement-card[data-status='missing']",
  ":where(.measurement-cards) .measurement-card__heading",
  ":is(h2, .heading).measurement-card__heading",
];

console.log("--- selector specificities ---");
console.log("selector".padEnd(58) + "specificity".padStart(12) + "  depth");
for (const s of SELECTORS) {
  const [a, b, c] = specificity(s);
  const depth = s.split(/[\s>+~]+/).filter(Boolean).length;
  console.log(s.padEnd(58) + `(${a}, ${b}, ${c})`.padStart(12) + String(depth).padStart(10));
}

// --- checking the name pattern: block__element--modifier ---
const PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*(__[a-z][a-z0-9]*(-[a-z0-9]+)*)?(--[a-z][a-z0-9]*(-[a-z0-9]+)*)?$/;
const NAMES = [
  "measurement-card", "measurement-card__heading", "measurement-card--wide", "measurement-card__value--missing",
  "measurement-card__body__row", "MeasurementCard", "measurement_card", "card--", "measurement-card__heading--short",
];

console.log("\n--- name pattern check ---");
for (const name of NAMES) {
  const parts = name.split("__");
  const multiElement = parts.length > 2;
  const valid = PATTERN.test(name) && !multiElement;
  const reason = multiElement ? "nested element name" : !PATTERN.test(name) ? "off-pattern spelling" : "-";
  console.log(`  ${name.padEnd(34)} ${(valid ? "valid" : "invalid").padEnd(9)} ${reason}`);
}

// --- the specificity distribution the same component's rules produce in two notations ---
console.log("\n--- specificity of all of one component's rules ---");
const NESTED = [
  ".measurement-cards .card",
  ".measurement-cards .card .heading",
  ".measurement-cards .card .heading span",
  ".measurement-cards .card.wide .heading",
];
const FLAT = [".measurement-card", ".measurement-card__heading", ".measurement-card__unit", ".measurement-card--wide .measurement-card__heading"];
const distribution = (list) => list.map((s) => specificity(s).join("."));
console.log("  nested notation:", distribution(NESTED).join("  "));
console.log("  flat notation  :", distribution(FLAT).join("  "));
--- selector specificities ---
selector                                                   specificity  depth
#content .measurements-section ul li a.active                (1, 2, 3)         5
.station-layout .measurements-section .measurement-cards .card .heading   (0, 5, 0)         5
section.measurements-section > ul > li                       (0, 1, 3)         3
.measurement-card__heading                                   (0, 1, 0)         1
.measurement-card--warning                                   (0, 1, 0)         1
.measurement-card:hover                                      (0, 2, 0)         1
.measurement-card[data-status='missing']                     (0, 2, 0)         1
:where(.measurement-cards) .measurement-card__heading        (0, 1, 0)         2
:is(h2, .heading).measurement-card__heading                  (0, 2, 0)         2

--- name pattern check ---
  measurement-card                   valid     -
  measurement-card__heading          valid     -
  measurement-card--wide             valid     -
  measurement-card__value--missing   valid     -
  measurement-card__body__row        invalid   nested element name
  MeasurementCard                    invalid   off-pattern spelling
  measurement_card                   invalid   off-pattern spelling
  card--                             invalid   off-pattern spelling
  measurement-card__heading--short   valid     -

--- specificity of all of one component's rules ---
  nested notation: 0.2.0  0.3.0  0.3.1  0.4.0
  flat notation  : 0.1.0  0.1.0  0.1.0  0.2.0

The last block fits the methodology’s entire justification into two lines. In the nested notation, the same component’s four rules sit at four different specificities; overriding one with another requires knowing which selector is how specific. In the flat notation, three rules sit at the same specificity, and only write order decides the ordering between them.

Deciding between rules at the same specificity is easier than deciding between rules at different specificities: whichever is written later wins, no other computation is needed.

The table above shows two more tools. A selector wrapped in :where() adds nothing to specificity — this is the way to write a container without growing the triple. :is(), on the other hand, counts the most specific branch inside it, and so does grow the triple.

The Pattern Being Checkable

A naming methodology stays practical only if it can be checked by machine. The second block tests nine names against a single regular expression and rejects four of them.

Three of the rejected ones are spelling-rule mistakes: an uppercase letter, a misused underscore separator, an empty modifier. The fourth is more interesting: measurement-card__body__row follows the spelling rules but contains two element names.

This prohibition is deliberate. A nested element name embeds the document’s tree structure into the name; when the heading is pulled out of the card’s body, the name becomes wrong. A block is flat: every element belongs directly to the block, and the tree relation between them is not written into the name.

The same check also finds names that exist in the stylesheet but are never used in the document. Since the name pattern is fixed, comparing the two sets is a matter of string matching.

Modifier or State

A modifier is a variant of the component known at design time: a wide card, a warning card. It is written when the document is produced and never changes over its lifetime.

State, on the other hand, changes at runtime: open, selected, loading, errored. If the two are written with the same mechanism, which class a script is allowed to change becomes unclear.

The way to keep the distinction is to carry state through a separate mechanism. There are two options, and both work with either data- attributes or aria- attributes:

.measurement-card[data-status="missing"] { border-color: var(--warning); }
.measurement-card[aria-expanded="true"] .measurement-card__body { display: block; }

The second is preferred. If the state is already announced to the accessibility tree, style reads that same announcement; a single source does two jobs. Writing a separate state class means keeping the same information in two places, and the two can drift apart.

Layer Prefixes

Naming methodologies organize the inside of a component; to organize the space between components, adding a layer prefix to the name is common. Prefixes vary by approach, but the function is the same: the name tells which layer a rule comes from.

The layers usually separated out are: layout rules that build the page skeleton, portable components, single-declaration utility classes, and runtime states. This separation can also be built with @layer, in which case there is no need to write it into the name — cascade layers already announce a rule’s order too. Layers are taken up in this topic’s sixth lesson.

Renaming the Station Page

/* components/measurement-card.css — step 1: all of the component's rules in one file */
.measurement-card {
  display: grid;
  gap: var(--spacing-0);
  padding: var(--spacing-1);
  border: 1px solid var(--line);
  transition: transform 240ms ease-out;
}

.measurement-card__heading { font-size: 1rem; color: var(--text-muted); }
.measurement-card__value   { font-size: 2rem; font-variant-numeric: tabular-nums; }
.measurement-card__unit    { color: var(--text-muted); }

.measurement-card--wide    { grid-column: span 2; }
.measurement-card--summary { grid-column: 1 / -1; }

.measurement-card[data-status="missing"] .measurement-card__value { color: var(--warning); }

.measurement-card:hover,
.measurement-card:focus-within { transform: translateY(-8px) scale(1.04); }

The component’s visual, layout, and motion declarations are gathered into one file. Understanding how a card looked when split across three files required reading three files; now there is one file per component, and the file name can be read from the class name’s root.

The container’s grid definition — how the cards line up side by side — is not in this file. The component knows its own inside, not its outside; outer layout stays in the .measurement-cards rule.

Summary

  • A naming methodology derives a name from the component it belongs to, not from the element’s place in the document; it takes on ownership, specificity creep, and deletability together.
  • A block is a portable component, an element is a part belonging to the block, a modifier is a variant; element names do not nest, because tree structure is not written into the name.
  • In flat notation, all of a component’s rules sit at the same specificity and only write order decides between them; in nested notation, every rule lands at a different specificity.
  • :where() adds nothing to specificity, :is() counts the most specific branch inside it; both are tools for keeping the triple under control.
  • A design-time variant is written with a modifier, a runtime state with an attribute; if accessibility attributes exist, style reads them.

Next Step

Every rule in the component file refers to names like var(--line), var(--warning), var(--spacing-1). These names were defined in the Visual Presentation with CSS course and kept on the root element. This single-centered structure is not enough once a component needs to set its own scale, a section needs to carry its own theme, or a modifier needs to change only a few values. The next lesson takes up custom properties as an architectural tool.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close